Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d758f7c6df |
@@ -2,6 +2,11 @@ version: "{branch}.{build}"
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- TOXENV: py35-no-ext
|
||||
PYTHON: "C:\\Python35-x64"
|
||||
PYTHON_VERSION: "3.5.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py36-no-ext
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
PYTHON_VERSION: "3.6.x"
|
||||
@@ -12,17 +17,6 @@ environment:
|
||||
PYTHON_VERSION: "3.7.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py38-no-ext
|
||||
PYTHON: "C:\\Python38-x64"
|
||||
PYTHON_VERSION: "3.8.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
# - TOXENV: py39-no-ext
|
||||
# PYTHON: "C:\\Python39-x64\\python"
|
||||
# PYTHONPATH: "C:\\Python39-x64"
|
||||
# PYTHON_VERSION: "3.9.x"
|
||||
# PYTHON_ARCH: "64"
|
||||
|
||||
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
|
||||
install:
|
||||
|
||||
@@ -5,11 +5,3 @@ omit = site-packages, sanic/utils.py, sanic/__main__.py
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
no cov
|
||||
no qa
|
||||
noqa
|
||||
NOQA
|
||||
pragma: no cover
|
||||
|
||||
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -1,3 +0,0 @@
|
||||
* @sanic-org/sanic-release-managers
|
||||
/sanic/ @sanic-org/framework
|
||||
/tests/ @sanic-org/framework
|
||||
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: sanic-org # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions and Help
|
||||
url: https://community.sanicframework.org/c/questions-and-help
|
||||
about: Do you need help with Sanic? Ask your questions here.
|
||||
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Help wanted
|
||||
about: Do you need help? Try community.sanicframework.org
|
||||
|
||||
---
|
||||
|
||||
*DELETE ALL BEFORE POSTING*
|
||||
*Post your HELP WANTED questions on [the community forum](https://community.sanicframework.org/)*.
|
||||
|
||||
Checkout the community forum before posting any question here.
|
||||
We prefer if you put these kinds of questions here:
|
||||
|
||||
https://community.sanicframework.org/c/questions-and-help
|
||||
20
.github/stale.yml
vendored
20
.github/stale.yml
vendored
@@ -1,20 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 90
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- bug
|
||||
- urgent
|
||||
- necessary
|
||||
- help wanted
|
||||
- RFC
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. If this
|
||||
is incorrect, please respond with an update. Thank you for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
67
.github/workflows/codeql-analysis.yml
vendored
67
.github/workflows/codeql-analysis.yml
vendored
@@ -1,67 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '25 16 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,11 +10,8 @@ coverage
|
||||
settings.py
|
||||
.idea/*
|
||||
.cache/*
|
||||
.mypy_cache/
|
||||
.python-version
|
||||
docs/_build/
|
||||
docs/_api/
|
||||
build/*
|
||||
.DS_Store
|
||||
dist/*
|
||||
pip-wheel-metadata/
|
||||
|
||||
81
.travis.yml
81
.travis.yml
@@ -5,79 +5,26 @@ cache:
|
||||
- $HOME/.cache/pip
|
||||
matrix:
|
||||
include:
|
||||
- env: TOX_ENV=py35
|
||||
python: 3.5
|
||||
- env: TOX_ENV=py35-no-ext
|
||||
python: 3.5
|
||||
- env: TOX_ENV=py36
|
||||
python: 3.6
|
||||
- env: TOX_ENV=py36-no-ext
|
||||
python: 3.6
|
||||
- env: TOX_ENV=py37
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 with Extensions"
|
||||
- env: TOX_ENV=py37-no-ext
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 without Extensions"
|
||||
- env: TOX_ENV=py38
|
||||
python: 3.8
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.8 with Extensions"
|
||||
- env: TOX_ENV=py38-no-ext
|
||||
python: 3.8
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.8 without Extensions"
|
||||
- env: TOX_ENV=py39
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
sudo: true
|
||||
name: "Python 3.9 with Extensions"
|
||||
- env: TOX_ENV=py39-no-ext
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
sudo: true
|
||||
name: "Python 3.9 without Extensions"
|
||||
- env: TOX_ENV=type-checking
|
||||
python: 3.7
|
||||
name: "Python 3.7 Type checks"
|
||||
- env: TOX_ENV=type-checking
|
||||
python: 3.8
|
||||
name: "Python 3.8 Type checks"
|
||||
- env: TOX_ENV=type-checking
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
name: "Python 3.9 Type checks"
|
||||
- env: TOX_ENV=security
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 Bandit security scan"
|
||||
- env: TOX_ENV=security
|
||||
python: 3.8
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.8 Bandit security scan"
|
||||
- env: TOX_ENV=security
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
sudo: true
|
||||
name: "Python 3.9 Bandit security scan"
|
||||
- env: TOX_ENV=docs
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 Documentation tests"
|
||||
- env: TOX_ENV=pyNightly
|
||||
python: "nightly"
|
||||
name: "Python nightly with Extensions"
|
||||
- env: TOX_ENV=pyNightly-no-ext
|
||||
python: "nightly"
|
||||
name: "Python nightly without Extensions"
|
||||
allow_failures:
|
||||
- env: TOX_ENV=pyNightly
|
||||
python: "nightly"
|
||||
name: "Python nightly with Extensions"
|
||||
- env: TOX_ENV=pyNightly-no-ext
|
||||
python: "nightly"
|
||||
name: "Python nightly without Extensions"
|
||||
- env: TOX_ENV=lint
|
||||
python: 3.6
|
||||
- env: TOX_ENV=check
|
||||
python: 3.6
|
||||
install:
|
||||
- pip install -U tox
|
||||
- pip install codecov
|
||||
@@ -86,9 +33,9 @@ after_success:
|
||||
- codecov
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: brewmaster
|
||||
user: channelcat
|
||||
password:
|
||||
secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys="
|
||||
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
|
||||
on:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
99
CHANGELOG.md
Normal file
99
CHANGELOG.md
Normal file
@@ -0,0 +1,99 @@
|
||||
Version 0.8
|
||||
-----------
|
||||
0.8.3
|
||||
- Changes:
|
||||
- Ownership changed to org 'huge-success'
|
||||
|
||||
0.8.0
|
||||
- Changes:
|
||||
- Add Server-Sent Events extension (Innokenty Lebedev)
|
||||
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
|
||||
- Sanitize URL before redirection (aveao)
|
||||
- Add url_bytes to request (johndoe46)
|
||||
- py37 support for travisci (yunstanford)
|
||||
- Auto reloader support for OSX (garyo)
|
||||
- Add UUID route support (Volodymyr Maksymiv)
|
||||
- Add pausable response streams (Ashley Sommer)
|
||||
- Add weakref to request slots (vopankov)
|
||||
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
|
||||
- Allow streaming handlers in add_route (kinware)
|
||||
- use travis_retry for tox (Raphael Deem)
|
||||
- update aiohttp version for test client (yunstanford)
|
||||
- add redirect import for clarity (yingshaoxo)
|
||||
- Update HTTP Entity headers (Arnulfo Solís)
|
||||
- Add register_listener method (Stephan Fitzpatrick)
|
||||
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
|
||||
- Content-length header on 204/304 responses (Arnulfo Solís)
|
||||
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
||||
- Update development status from pre-alpha to beta (Maksim Anisenkov)
|
||||
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
||||
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
||||
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
||||
- Add support for blueprint groups and nesting (Elias Tarhini)
|
||||
- Remove uvloop for windows setup (Aleksandr Kurlov)
|
||||
- Auto Reload (Yaser Amari)
|
||||
- Documentation updates/fixups (multiple contributors)
|
||||
|
||||
- Fixes:
|
||||
- Fix: auto_reload in Linux (Ashley Sommer)
|
||||
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
|
||||
- Fix: disable auto_reload by default on windows (abuckenheimer)
|
||||
- Fix (1143): Turn off access log with gunicorn (hqy)
|
||||
- Fix (1268): Support status code for file response (Cosmo Borsky)
|
||||
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
|
||||
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
|
||||
- Fix (1242): Responses for CI header (yunstanford)
|
||||
- Fix (1237): add version constraint for websockets (yunstanford)
|
||||
- Fix (1231): memory leak - always release resource (Phillip Xu)
|
||||
- Fix (1221): make request truthy if transport exists (Raphael Deem)
|
||||
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
|
||||
- Fix try_everything examples (PyManiacGR, kot83)
|
||||
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
|
||||
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
|
||||
- Fix: raw requires bytes-like object (cloudship)
|
||||
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
|
||||
- Fix: Bug in multipart/form-data parser (DirkGuijt)
|
||||
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
|
||||
- Fix: Parameter check (Howie Hu)
|
||||
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
|
||||
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
|
||||
- Fix: single quote in readme.rst (Cosven)
|
||||
- Fix: method typos (Dmitry Dygalo)
|
||||
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
|
||||
- Fix (1042): Exception Handling (Raphael Deem)
|
||||
- Fix: Chinese URIs (Howie Hu)
|
||||
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
|
||||
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
|
||||
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
|
||||
- Fix (1065): allow add_task after server starts (Raphael Deem)
|
||||
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
|
||||
- Fix (1062): inject the app in add_task method (Raphael Deem)
|
||||
- Fix: update environment.yml for readthedocs (Eli Uriegas)
|
||||
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
|
||||
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
|
||||
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
|
||||
|
||||
Note: Changelog was unmaintained between 0.1 and 0.7
|
||||
|
||||
Version 0.1
|
||||
-----------
|
||||
- 0.1.7
|
||||
- Reversed static url and directory arguments to meet spec
|
||||
- 0.1.6
|
||||
- Static files
|
||||
- Lazy Cookie Loading
|
||||
- 0.1.5
|
||||
- Cookies
|
||||
- Blueprint listeners and ordering
|
||||
- Faster Router
|
||||
- Fix: Incomplete file reads on medium+ sized post requests
|
||||
- Breaking: after_start and before_stop now pass sanic as their first argument
|
||||
- 0.1.4
|
||||
- Multiprocessing
|
||||
- 0.1.3
|
||||
- Blueprint support
|
||||
- Faster Response processing
|
||||
- 0.1.1 - 0.1.2
|
||||
- Struggling to update pypi via CI
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
1094
CHANGELOG.rst
1094
CHANGELOG.rst
File diff suppressed because it is too large
Load Diff
72
CONTRIBUTING.md
Normal file
72
CONTRIBUTING.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
We are committed to providing a friendly, safe and welcoming environment for all,
|
||||
regardless of gender, sexual orientation, disability, ethnicity, religion,
|
||||
or similar personal characteristic.
|
||||
Our [code of conduct](./CONDUCT.md) sets the standards for behavior.
|
||||
|
||||
## Installation
|
||||
|
||||
To develop on sanic (and mainly to just run the tests) it is highly recommend to
|
||||
install from sources.
|
||||
|
||||
So assume you have already cloned the repo and are in the working directory with
|
||||
a virtual environment already set up, then run:
|
||||
|
||||
```bash
|
||||
python setup.py develop && pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
```bash
|
||||
tox
|
||||
```
|
||||
|
||||
See it's that simple!
|
||||
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
1. All pull requests must pass unit tests.
|
||||
2. All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project.
|
||||
3. All pull requests must pass flake8 checks.
|
||||
4. All pull requests must be consistent with the existing code.
|
||||
5. If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
6. If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
7. An example must be one of the following:
|
||||
* Example of how to use Sanic
|
||||
* Example of how to use Sanic extensions
|
||||
* Example of how to use Sanic and asynchronous library
|
||||
|
||||
## Documentation
|
||||
|
||||
Sanic's documentation is built
|
||||
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
|
||||
Markdown and can be found in the `docs` folder, while the module reference is
|
||||
automatically generated using `sphinx-apidoc`.
|
||||
|
||||
To generate the documentation from scratch:
|
||||
|
||||
```bash
|
||||
sphinx-apidoc -fo docs/_api/ sanic
|
||||
sphinx-build -b html docs docs/_build
|
||||
```
|
||||
|
||||
The HTML documentation will be created in the `docs/_build` folder.
|
||||
|
||||
## Warning
|
||||
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||
Sanic without significant gains in usability, security, or features may not be
|
||||
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||
idea, open an issue for discussion and help.
|
||||
233
CONTRIBUTING.rst
233
CONTRIBUTING.rst
@@ -1,233 +0,0 @@
|
||||
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files,
|
||||
or helping with the `Sanic User Guide <https://github.com/sanic-org/sanic-guide>`_
|
||||
by providing documentation or implementation examples would be appreciated!
|
||||
|
||||
We are committed to providing a friendly, safe and welcoming environment for all,
|
||||
regardless of gender, sexual orientation, disability, ethnicity, religion,
|
||||
or similar personal characteristic.
|
||||
Our `code of conduct <https://github.com/sanic-org/sanic/blob/master/CONDUCT.md>`_ sets the standards for behavior.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
To develop on Sanic (and mainly to just run the tests) it is highly recommend to
|
||||
install from sources.
|
||||
|
||||
So assume you have already cloned the repo and are in the working directory with
|
||||
a virtual environment already set up, then run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e . ".[dev]"
|
||||
|
||||
Dependency Changes
|
||||
------------------
|
||||
|
||||
``Sanic`` doesn't use ``requirements*.txt`` files to manage any kind of dependencies related to it in order to simplify the
|
||||
effort required in managing the dependencies. Please make sure you have read and understood the following section of
|
||||
the document that explains the way ``sanic`` manages dependencies inside the ``setup.py`` file.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Dependency Type
|
||||
- Usage
|
||||
- Installation
|
||||
* - requirements
|
||||
- Bare minimum dependencies required for sanic to function
|
||||
- ``pip3 install -e .``
|
||||
* - tests_require / extras_require['test']
|
||||
- Dependencies required to run the Unit Tests for ``sanic``
|
||||
- ``pip3 install -e '.[test]'``
|
||||
* - extras_require['dev']
|
||||
- Additional Development requirements to add contributing
|
||||
- ``pip3 install -e '.[dev]'``
|
||||
* - extras_require['docs']
|
||||
- Dependencies required to enable building and enhancing sanic documentation
|
||||
- ``pip3 install -e '.[docs]'``
|
||||
|
||||
|
||||
Running all tests
|
||||
-----------------
|
||||
|
||||
To run the tests for Sanic it is recommended to use tox like so:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox
|
||||
|
||||
See it's that simple!
|
||||
|
||||
``tox.ini`` contains different environments. Running ``tox`` without any arguments will
|
||||
run all unittests, perform lint and other checks.
|
||||
|
||||
Run unittests
|
||||
-------------
|
||||
|
||||
``tox`` environment -> ``[testenv]``
|
||||
|
||||
To execute only unittests, run ``tox`` with environment like so:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox -e py36 -v -- tests/test_config.py
|
||||
# or
|
||||
tox -e py37 -v -- tests/test_config.py
|
||||
|
||||
Run lint checks
|
||||
---------------
|
||||
|
||||
``tox`` environment -> ``[testenv:lint]``
|
||||
|
||||
Permform ``flake8``\ , ``black`` and ``isort`` checks.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox -e lint
|
||||
|
||||
Run type annotation checks
|
||||
---------------
|
||||
|
||||
``tox`` environment -> ``[testenv:type-checking]``
|
||||
|
||||
Permform ``mypy`` checks.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox -e type-checking
|
||||
|
||||
Run other checks
|
||||
----------------
|
||||
|
||||
``tox`` environment -> ``[testenv:check]``
|
||||
|
||||
Perform other checks.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox -e check
|
||||
|
||||
Run Static Analysis
|
||||
-------------------
|
||||
|
||||
``tox`` environment -> ``[testenv:security]``
|
||||
|
||||
Perform static analysis security scan
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox -e security
|
||||
|
||||
Run Documentation sanity check
|
||||
------------------------------
|
||||
|
||||
``tox`` environment -> ``[testenv:docs]``
|
||||
|
||||
Perform sanity check on documentation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tox -e docs
|
||||
|
||||
|
||||
Code Style
|
||||
----------
|
||||
|
||||
To maintain the code consistency, Sanic uses following tools.
|
||||
|
||||
|
||||
#. `isort <https://github.com/timothycrosley/isort>`_
|
||||
#. `black <https://github.com/python/black>`_
|
||||
#. `flake8 <https://github.com/PyCQA/flake8>`_
|
||||
|
||||
isort
|
||||
*****
|
||||
|
||||
``isort`` sorts Python imports. It divides imports into three
|
||||
categories sorted each in alphabetical order.
|
||||
|
||||
|
||||
#. built-in
|
||||
#. third-party
|
||||
#. project-specific
|
||||
|
||||
black
|
||||
*****
|
||||
|
||||
``black`` is a Python code formatter.
|
||||
|
||||
flake8
|
||||
******
|
||||
|
||||
``flake8`` is a Python style guide that wraps following tools into one.
|
||||
|
||||
|
||||
#. PyFlakes
|
||||
#. pycodestyle
|
||||
#. Ned Batchelder's McCabe script
|
||||
|
||||
``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks.
|
||||
|
||||
The **easiest** way to make your code conform is to run the following before committing.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make pretty
|
||||
|
||||
Refer `tox <https://tox.readthedocs.io/en/latest/index.html>`_ documentation for more details.
|
||||
|
||||
Pull requests
|
||||
-------------
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
|
||||
#. All pull requests must pass unit tests.
|
||||
#. All pull requests must be reviewed and approved by at least one current member of the Core Developer team.
|
||||
#. All pull requests must pass flake8 checks.
|
||||
#. All pull requests must match ``isort`` and ``black`` requirements.
|
||||
#. All pull requests must be **PROPERLY** type annotated, unless exemption is given.
|
||||
#. All pull requests must be consistent with the existing code.
|
||||
#. If you decide to remove/change anything from any common interface a deprecation message should accompany it in accordance with our `deprecation policy <https://sanicframework.org/en/guide/project/policies.html#deprecation>`_.
|
||||
#. If you implement a new feature you should have at least one unit test to accompany it.
|
||||
#. An example must be one of the following:
|
||||
|
||||
* Example of how to use Sanic
|
||||
* Example of how to use Sanic extensions
|
||||
* Example of how to use Sanic and asynchronous library
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Sanic's API documentation is built using `sphinx <http://www.sphinx-doc.org/en/1.5.1/>`_ with module references
|
||||
automatically generated using ``sphinx-apidoc``.
|
||||
|
||||
The User Guide is in the `sanic-guide <https://github.com/sanic-org/sanic-guide>`_ repository.
|
||||
|
||||
To generate the documentation from scratch:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sphinx-apidoc -fo docs/_api/ sanic
|
||||
sphinx-build -b html docs docs/_build
|
||||
|
||||
# There is a simple make command provided to ease the work required in generating
|
||||
# the documentation
|
||||
make docs
|
||||
|
||||
The HTML documentation will be created in the ``docs/_build`` folder.
|
||||
|
||||
You can run the following to have a live development server with the API documents
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make docs-serve
|
||||
|
||||
Refer to the User Guide repo for documentation on how to contribute there.
|
||||
|
||||
.. warning::
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||
Sanic without significant gains in usability, security, or features may not be
|
||||
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||
idea, open an issue for discussion and help.
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-present Sanic Community
|
||||
Copyright (c) 2016-present Channel Cat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
16
MANIFEST.in
16
MANIFEST.in
@@ -1,15 +1,7 @@
|
||||
# Non Code related contents
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include pyproject.toml
|
||||
|
||||
# Setup
|
||||
include MANIFEST.in
|
||||
include LICENSE
|
||||
include setup.py
|
||||
include Makefile
|
||||
|
||||
# Tests
|
||||
include .coveragerc
|
||||
graft tests
|
||||
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
100
Makefile
100
Makefile
@@ -1,100 +1,4 @@
|
||||
.PHONY: help test test-coverage install docker-test black fix-import beautify
|
||||
|
||||
.DEFAULT: help
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo "test"
|
||||
@echo " Run Sanic Unit Tests"
|
||||
@echo "test-coverage"
|
||||
@echo " Run Sanic Unit Tests with Coverage"
|
||||
@echo "install"
|
||||
@echo " Install Sanic"
|
||||
@echo "docker-test"
|
||||
@echo " Run Sanic Unit Tests using Docker"
|
||||
@echo "black"
|
||||
@echo " Analyze and fix linting issues using Black"
|
||||
@echo "fix-import"
|
||||
@echo " Analyze and fix import order using isort"
|
||||
@echo "beautify [sort_imports=1] [include_tests=1]"
|
||||
@echo " Analyze and fix linting issue using black and optionally fix import sort using isort"
|
||||
@echo ""
|
||||
@echo "docs"
|
||||
@echo " Generate Sanic documentation"
|
||||
@echo ""
|
||||
@echo "clean-docs"
|
||||
@echo " Clean Sanic documentation"
|
||||
@echo ""
|
||||
@echo "docs-test"
|
||||
@echo " Test Sanic Documentation for errors"
|
||||
@echo ""
|
||||
@echo "changelog"
|
||||
@echo " Generate changelog for Sanic to prepare for new release"
|
||||
@echo ""
|
||||
@echo "release"
|
||||
@echo " Prepare Sanic for a new changes by version bump and changelog"
|
||||
@echo ""
|
||||
|
||||
|
||||
clean:
|
||||
find . ! -path "./.eggs/*" -name "*.pyc" -exec rm {} \;
|
||||
find . ! -path "./.eggs/*" -name "*.pyo" -exec rm {} \;
|
||||
find . ! -path "./.eggs/*" -name ".coverage" -exec rm {} \;
|
||||
rm -rf build/* > /dev/null 2>&1
|
||||
rm -rf dist/* > /dev/null 2>&1
|
||||
|
||||
test: clean
|
||||
python setup.py test
|
||||
|
||||
test-coverage: clean
|
||||
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
|
||||
|
||||
install:
|
||||
python setup.py install
|
||||
|
||||
docker-test: clean
|
||||
test:
|
||||
find . -name "*.pyc" -delete
|
||||
docker build -t sanic/test-image -f docker/Dockerfile .
|
||||
docker run -t sanic/test-image tox
|
||||
|
||||
beautify: black
|
||||
ifdef sort_imports
|
||||
ifdef include_tests
|
||||
$(warning It is suggested that you do not run sort import on tests)
|
||||
isort -rc sanic tests
|
||||
else
|
||||
$(info Sorting Imports)
|
||||
isort -rc sanic tests --profile=black
|
||||
endif
|
||||
endif
|
||||
|
||||
black:
|
||||
black --config ./.black.toml sanic tests
|
||||
|
||||
isort:
|
||||
isort sanic tests --profile=black
|
||||
|
||||
pretty: black isort
|
||||
|
||||
docs-clean:
|
||||
cd docs && make clean
|
||||
|
||||
docs: docs-clean
|
||||
cd docs && make html
|
||||
|
||||
docs-test: docs-clean
|
||||
cd docs && make dummy
|
||||
|
||||
docs-serve:
|
||||
# python -m http.server --directory=./docs/_build/html 9999
|
||||
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
|
||||
|
||||
changelog:
|
||||
python scripts/changelog.py
|
||||
|
||||
release:
|
||||
ifdef version
|
||||
python scripts/release.py --release-version ${version} --generate-changelog
|
||||
else
|
||||
python scripts/release.py --generate-changelog
|
||||
endif
|
||||
|
||||
|
||||
205
README.rst
205
README.rst
@@ -1,114 +1,15 @@
|
||||
.. image:: https://raw.githubusercontent.com/sanic-org/sanic-assets/master/png/sanic-framework-logo-400x97.png
|
||||
:alt: Sanic | Build fast. Run fast.
|
||||
Sanic
|
||||
=====
|
||||
|
||||
Sanic | Build fast. Run fast.
|
||||
=============================
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
|
||||
|
||||
.. start-badges
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
||||
|
||||
.. list-table::
|
||||
:widths: 15 85
|
||||
:stub-columns: 1
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
|
||||
* - Build
|
||||
- | |Build Status| |AppVeyor Build Status| |Codecov|
|
||||
* - Docs
|
||||
- | |UserGuide| |Documentation|
|
||||
* - Package
|
||||
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
|
||||
* - Support
|
||||
- | |Forums| |Discord| |Awesome|
|
||||
* - Stats
|
||||
- | |Downloads| |WkDownloads| |Conda downloads|
|
||||
Sanic is developed `on GitHub <https://github.com/huge-success/sanic/>`_. We also have `a community discussion board <https://community.sanicframework.org/>`_. Contributions are welcome!
|
||||
|
||||
.. |UserGuide| image:: https://img.shields.io/badge/user%20guide-sanic-ff0068
|
||||
:target: https://sanicframework.org/
|
||||
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
|
||||
:target: https://community.sanicframework.org/
|
||||
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
|
||||
:target: https://discord.gg/FARQzAEMAA
|
||||
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/sanic-org/sanic
|
||||
.. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master
|
||||
:target: https://travis-ci.com/sanic-org/sanic
|
||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||
:target: https://ci.appveyor.com/project/sanic-org/sanic
|
||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
.. |Wheel| image:: https://img.shields.io/pypi/wheel/sanic.svg
|
||||
:alt: PyPI Wheel
|
||||
:target: https://pypi.python.org/pypi/sanic
|
||||
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
|
||||
:alt: Supported implementations
|
||||
:target: https://pypi.python.org/pypi/sanic
|
||||
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
|
||||
:alt: Awesome Sanic List
|
||||
:target: https://github.com/mekicha/awesome-sanic
|
||||
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
|
||||
:alt: Downloads
|
||||
:target: https://pepy.tech/project/sanic
|
||||
.. |WkDownloads| image:: https://pepy.tech/badge/sanic/week
|
||||
:alt: Downloads
|
||||
:target: https://pepy.tech/project/sanic
|
||||
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
|
||||
:alt: Downloads
|
||||
:target: https://anaconda.org/conda-forge/sanic
|
||||
|
||||
.. end-badges
|
||||
|
||||
Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||
|
||||
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
|
||||
|
||||
`Source code on GitHub <https://github.com/sanic-org/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_ | `User Guide <https://sanicframework.org>`_ | `Chat on Discord <https://discord.gg/FARQzAEMAA>`_
|
||||
|
||||
The project is maintained by the community, for the community. **Contributions are welcome!**
|
||||
|
||||
The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.
|
||||
|
||||
Sponsor
|
||||
-------
|
||||
|
||||
|Try CodeStream|
|
||||
|
||||
.. |Try CodeStream| image:: https://alt-images.codestream.com/codestream_logo_sanicorg.png
|
||||
:target: https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner
|
||||
:alt: Try CodeStream
|
||||
|
||||
Manage pull requests and conduct code reviews in your IDE with full source-tree context. Comment on any line, not just the diffs. Use jump-to-definition, your favorite keybindings, and code intelligence with more of your workflow.
|
||||
|
||||
`Learn More <https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner>`_
|
||||
|
||||
Thank you to our sponsor. Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
``pip3 install sanic``
|
||||
|
||||
Sanic makes use of ``uvloop`` and ``ujson`` to help with performance. If you do not want to use those packages, simply add an environmental variable ``SANIC_NO_UVLOOP=true`` or ``SANIC_NO_UJSON=true`` at install time.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
$ export SANIC_NO_UVLOOP=true
|
||||
$ export SANIC_NO_UJSON=true
|
||||
$ pip3 install --no-binary :all: sanic
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
|
||||
use ``sanic`` with ``ujson`` dependency.
|
||||
|
||||
.. note::
|
||||
|
||||
Windows support is currently "experimental" and on a best-effort basis. Multiple workers are also not currently supported on Windows (see `Issue #1517 <https://github.com/sanic-org/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully.
|
||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/huge-success/sanic/issues/396>`_ that we use to track those projects!
|
||||
|
||||
Hello World Example
|
||||
-------------------
|
||||
@@ -118,55 +19,85 @@ Hello World Example
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic("My Hello, world app")
|
||||
app = Sanic()
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
return json({'hello': 'world'})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
Sanic can now be easily run using ``sanic hello.app``.
|
||||
Installation
|
||||
------------
|
||||
|
||||
.. code::
|
||||
- ``pip install sanic``
|
||||
|
||||
[2018-12-30 11:37:41 +0200] [13564] [INFO] Goin' Fast @ http://127.0.0.1:8000
|
||||
[2018-12-30 11:37:41 +0200] [13564] [INFO] Starting worker [13564]
|
||||
To install sanic without uvloop or ujson using bash, you can provide either or both of these environmental variables
|
||||
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
|
||||
installation.
|
||||
|
||||
And, we can verify it is working: ``curl localhost:8000 -i``
|
||||
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip install sanic``
|
||||
|
||||
.. code::
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Connection: keep-alive
|
||||
Keep-Alive: 5
|
||||
Content-Length: 17
|
||||
Content-Type: application/json
|
||||
|
||||
{"hello":"world"}
|
||||
|
||||
**Now, let's go build something fast!**
|
||||
|
||||
Minimum Python version is 3.7. If you need Python 3.6 support, please use v20.12LTS.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
`User Guide <https://sanicframework.org>`__ and `API Documentation <http://sanic.readthedocs.io/>`__.
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
`Release Changelogs <https://github.com/sanic-org/sanic/blob/master/CHANGELOG.rst>`__.
|
||||
|
||||
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
|
||||
|
||||
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
|
||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/huge-success/sanic
|
||||
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/huge-success/sanic
|
||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||
:target: https://ci.appveyor.com/project/huge-success/sanic
|
||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
|
||||
Questions and Discussion
|
||||
------------------------
|
||||
|
||||
`Ask a question or join the conversation <https://community.sanicframework.org/>`__.
|
||||
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
|
||||
|
||||
Contribution
|
||||
------------
|
||||
|
||||
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
|
||||
Examples
|
||||
--------
|
||||
`Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
||||
|
||||
`Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
||||
|
||||
`Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
|
||||
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
::
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
|
||||
34
SECURITY.md
34
SECURITY.md
@@ -1,34 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
|
||||
|
||||
| Version | LTS | Supported |
|
||||
| ------- | ------------- | ------------------ |
|
||||
| 20.12 | until 2022-12 | :heavy_check_mark: |
|
||||
| 20.9 | | :x: |
|
||||
| 20.6 | | :x: |
|
||||
| 20.3 | | :x: |
|
||||
| 19.12 | until 2021-12 | :white_check_mark: |
|
||||
| 19.9 | | :x: |
|
||||
| 19.6 | | :x: |
|
||||
| 19.3 | | :x: |
|
||||
| 18.12 | | :x: |
|
||||
| 0.8.3 | | :x: |
|
||||
| 0.7.0 | | :x: |
|
||||
| 0.6.0 | | :x: |
|
||||
| 0.5.4 | | :x: |
|
||||
| 0.4.1 | | :x: |
|
||||
| 0.3.1 | | :x: |
|
||||
| 0.2.0 | | :x: |
|
||||
| 0.1.9 | | :x: |
|
||||
|
||||
:white_check_mark: = security/bug fixes
|
||||
:heavy_check_mark: = full support
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
|
||||
|
||||
This will help to not publicize the issue until the team can address it and resolve it.
|
||||
2
changelogs/.gitignore
vendored
2
changelogs/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
# Except this file
|
||||
!.gitignore
|
||||
@@ -1 +0,0 @@
|
||||
Remove [version] section.
|
||||
@@ -1,3 +0,0 @@
|
||||
Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values
|
||||
|
||||
Allows setting the ping_interval and ping_timeout arguments when initializing `WebSocketCommonProtocol`.
|
||||
@@ -1 +0,0 @@
|
||||
Adds py.typed file to expose type information to other packages.
|
||||
14
codecov.yml
14
codecov.yml
@@ -1,14 +0,0 @@
|
||||
codecov:
|
||||
require_ci_to_pass: no
|
||||
coverage:
|
||||
precision: 3
|
||||
round: nearest
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.75%
|
||||
4
docs/_static/custom.css
vendored
4
docs/_static/custom.css
vendored
@@ -1,4 +0,0 @@
|
||||
.wy-side-nav-search,
|
||||
.wy-nav-top {
|
||||
background: #444444;
|
||||
}
|
||||
BIN
docs/_static/sanic-framework-logo-white-400x97.png
vendored
BIN
docs/_static/sanic-framework-logo-white-400x97.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
91
docs/conf.py
91
docs/conf.py
@@ -10,12 +10,12 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add support for Markdown documentation using Recommonmark
|
||||
from recommonmark.parser import CommonMarkParser
|
||||
|
||||
# Add support for auto-doc
|
||||
import recommonmark
|
||||
|
||||
from recommonmark.transform import AutoStructify
|
||||
|
||||
|
||||
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
||||
# autogenerate documentation from docstrings
|
||||
root_directory = os.path.dirname(os.getcwd())
|
||||
@@ -23,25 +23,24 @@ sys.path.insert(0, root_directory)
|
||||
|
||||
import sanic
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = ["sphinx.ext.autodoc", "recommonmark"]
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio']
|
||||
|
||||
templates_path = ["_templates"]
|
||||
templates_path = ['_templates']
|
||||
|
||||
# Enable support for both Restructured Text and Markdown
|
||||
source_suffix = [".rst", ".md"]
|
||||
source_parsers = {'.md': CommonMarkParser}
|
||||
source_suffix = ['.rst', '.md']
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = "Sanic"
|
||||
copyright = "2021, Sanic Community Organization"
|
||||
author = "Sanic Community Organization"
|
||||
project = 'Sanic'
|
||||
copyright = '2016, Sanic contributors'
|
||||
author = 'Sanic contributors'
|
||||
|
||||
html_logo = "./_static/sanic-framework-logo-white-400x97.png"
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
@@ -56,7 +55,7 @@ release = sanic.__version__
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = "en"
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
@@ -64,10 +63,10 @@ language = "en"
|
||||
#
|
||||
# modules.rst is generated by sphinx-apidoc but is unused. This suppresses
|
||||
# a warning about it.
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "modules.rst"]
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'modules.rst']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = "sphinx"
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
@@ -76,17 +75,17 @@ todo_include_todos = False
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = ["custom.css"]
|
||||
html_static_path = ['_static']
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "Sanicdoc"
|
||||
htmlhelp_basename = 'Sanicdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
@@ -94,12 +93,15 @@ latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
@@ -108,21 +110,14 @@ latex_elements = {
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(
|
||||
master_doc,
|
||||
"Sanic.tex",
|
||||
"Sanic Documentation",
|
||||
"Sanic contributors",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||
'Sanic contributors', 'manual'), ]
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [(master_doc, "sanic", "Sanic Documentation", [author], 1)]
|
||||
man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
@@ -130,15 +125,8 @@ man_pages = [(master_doc, "sanic", "Sanic Documentation", [author], 1)]
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
master_doc,
|
||||
"Sanic",
|
||||
"Sanic Documentation",
|
||||
author,
|
||||
"Sanic",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
(master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
|
||||
'One line description of project.', 'Miscellaneous'),
|
||||
]
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
@@ -150,32 +138,17 @@ epub_publisher = author
|
||||
epub_copyright = copyright
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ["search.html"]
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
# -- Custom Settings -------------------------------------------------------
|
||||
|
||||
suppress_warnings = ["image.nonlocal_uri"]
|
||||
|
||||
|
||||
autodoc_typehints = "description"
|
||||
autodoc_default_options = {
|
||||
"member-order": "groupwise",
|
||||
}
|
||||
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": False,
|
||||
},
|
||||
True,
|
||||
)
|
||||
app.add_config_value('recommonmark_config', {
|
||||
'enable_eval_rst': True,
|
||||
'enable_auto_doc_ref': True,
|
||||
}, True)
|
||||
app.add_transform(AutoStructify)
|
||||
|
||||
|
||||
html_theme_options = {
|
||||
"style_external_links": False,
|
||||
}
|
||||
|
||||
468
docs/index.html
468
docs/index.html
@@ -1,468 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils 0.15.2: http://docutils.sourceforge.net/" />
|
||||
<title>index.rst</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
|
||||
<div class="section" id="sanic">
|
||||
<h1>Sanic</h1>
|
||||
<p>Sanic is a Python 3.6+ web server and web framework that's written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy.</p>
|
||||
<p>The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.</p>
|
||||
<p>Sanic is developed <a class="reference external" href="https://github.com/channelcat/sanic/">on GitHub</a>. Contributions are welcome!</p>
|
||||
<div class="section" id="sanic-aspires-to-be-simple">
|
||||
<h2>Sanic aspires to be simple</h2>
|
||||
<pre class="code python literal-block">
|
||||
<span class="keyword namespace">from</span> <span class="name namespace">sanic</span> <span class="keyword namespace">import</span> <span class="name">Sanic</span>
|
||||
<span class="keyword namespace">from</span> <span class="name namespace">sanic.response</span> <span class="keyword namespace">import</span> <span class="name">json</span>
|
||||
|
||||
<span class="name">app</span> <span class="operator">=</span> <span class="name">Sanic</span><span class="punctuation">()</span>
|
||||
|
||||
<span class="name decorator">@app.route</span><span class="punctuation">(</span><span class="literal string double">"/"</span><span class="punctuation">)</span>
|
||||
<span class="name">async</span> <span class="keyword">def</span> <span class="name function">test</span><span class="punctuation">(</span><span class="name">request</span><span class="punctuation">):</span>
|
||||
<span class="keyword">return</span> <span class="name">json</span><span class="punctuation">({</span><span class="literal string double">"hello"</span><span class="punctuation">:</span> <span class="literal string double">"world"</span><span class="punctuation">})</span>
|
||||
|
||||
<span class="keyword">if</span> <span class="name variable magic">__name__</span> <span class="operator">==</span> <span class="literal string double">"__main__"</span><span class="punctuation">:</span>
|
||||
<span class="name">app</span><span class="operator">.</span><span class="name">run</span><span class="punctuation">(</span><span class="name">host</span><span class="operator">=</span><span class="literal string double">"0.0.0.0"</span><span class="punctuation">,</span> <span class="name">port</span><span class="operator">=</span><span class="literal number integer">8000</span><span class="punctuation">)</span>
|
||||
</pre>
|
||||
<div class="admonition note">
|
||||
<p class="first admonition-title">Note</p>
|
||||
<p class="last">Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru
|
||||
December 2020. Official Python support for version 3.5 is set to expire in September 2020.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="guides">
|
||||
<h1>Guides</h1>
|
||||
<div class="system-message">
|
||||
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 6)</p>
|
||||
<p>Unknown directive type "toctree".</p>
|
||||
<pre class="literal-block">
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
sanic/getting_started
|
||||
sanic/config
|
||||
sanic/logging
|
||||
sanic/request_data
|
||||
sanic/response
|
||||
sanic/cookies
|
||||
sanic/routing
|
||||
sanic/blueprints
|
||||
sanic/static_files
|
||||
sanic/versioning
|
||||
sanic/exceptions
|
||||
sanic/middleware
|
||||
sanic/websocket
|
||||
sanic/decorators
|
||||
sanic/streaming
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/sockets
|
||||
sanic/ssl
|
||||
sanic/debug_mode
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
sanic/examples
|
||||
sanic/changelog
|
||||
sanic/contributing
|
||||
sanic/api_reference
|
||||
sanic/asyncio_python37
|
||||
|
||||
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="module-documentation">
|
||||
<h1>Module Documentation</h1>
|
||||
<div class="system-message">
|
||||
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 42)</p>
|
||||
<p>Unknown directive type "toctree".</p>
|
||||
<pre class="literal-block">
|
||||
.. toctree::
|
||||
|
||||
</pre>
|
||||
</div>
|
||||
<ul>
|
||||
<li><p class="first"><a href="#id1"><span class="problematic" id="id2">:ref:`genindex`</span></a></p>
|
||||
<div class="system-message" id="id1">
|
||||
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 44); <em><a href="#id2">backlink</a></em></p>
|
||||
<p>Unknown interpreted text role "ref".</p>
|
||||
</div>
|
||||
</li>
|
||||
<li><p class="first"><a href="#id3"><span class="problematic" id="id4">:ref:`modindex`</span></a></p>
|
||||
<div class="system-message" id="id3">
|
||||
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 45); <em><a href="#id4">backlink</a></em></p>
|
||||
<p>Unknown interpreted text role "ref".</p>
|
||||
</div>
|
||||
</li>
|
||||
<li><p class="first"><a href="#id5"><span class="problematic" id="id6">:ref:`search`</span></a></p>
|
||||
<div class="system-message" id="id5">
|
||||
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 46); <em><a href="#id6">backlink</a></em></p>
|
||||
<p>Unknown interpreted text role "ref".</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +1,36 @@
|
||||
.. include:: ../README.rst
|
||||
.. include:: sanic/index.rst
|
||||
|
||||
User Guide
|
||||
==========
|
||||
|
||||
To learn about using Sanic, checkout the `User Guide <https://sanicframework.org/guide/>`__.
|
||||
|
||||
API
|
||||
===
|
||||
Guides
|
||||
======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
👥 User Guide <https://sanicframework.org/guide/>
|
||||
sanic/api_reference
|
||||
💻 Source code <https://github.com/sanic-org/sanic/>
|
||||
sanic/changelog
|
||||
sanic/getting_started
|
||||
sanic/routing
|
||||
sanic/request_data
|
||||
sanic/response
|
||||
sanic/static_files
|
||||
sanic/exceptions
|
||||
sanic/middleware
|
||||
sanic/blueprints
|
||||
sanic/websocket
|
||||
sanic/config
|
||||
sanic/cookies
|
||||
sanic/decorators
|
||||
sanic/streaming
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/sockets
|
||||
sanic/ssl
|
||||
sanic/logging
|
||||
sanic/versioning
|
||||
sanic/debug_mode
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
sanic/contributing
|
||||
❓ Support <https://community.sanicframework.org/>
|
||||
💬 Chat <https://discord.gg/FARQzAEMAA>
|
||||
sanic/api_reference
|
||||
|
||||
|
||||
Module Documentation
|
||||
@@ -27,3 +40,4 @@ Module Documentation
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -1,132 +1,150 @@
|
||||
📑 API Reference
|
||||
================
|
||||
API Reference
|
||||
=============
|
||||
|
||||
sanic.app
|
||||
---------
|
||||
Submodules
|
||||
----------
|
||||
|
||||
sanic.app module
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.app
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.blueprints
|
||||
----------------
|
||||
sanic.blueprints module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: sanic.blueprints
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.blueprint_group
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.blueprint_group
|
||||
:members:
|
||||
:special-members:
|
||||
|
||||
|
||||
sanic.compat
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.compat
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.config
|
||||
------------
|
||||
sanic.config module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.config
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.cookies
|
||||
-------------
|
||||
sanic.constants module
|
||||
----------------------
|
||||
|
||||
.. automodule:: sanic.constants
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.cookies module
|
||||
--------------------
|
||||
|
||||
.. automodule:: sanic.cookies
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.errorpages
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.errorpages
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.exceptions
|
||||
----------------
|
||||
sanic.exceptions module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: sanic.exceptions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.handlers
|
||||
--------------
|
||||
sanic.handlers module
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.handlers
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.http
|
||||
----------
|
||||
|
||||
.. automodule:: sanic.http
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.log
|
||||
---------
|
||||
sanic.log module
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.log
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.request
|
||||
-------------
|
||||
sanic.request module
|
||||
--------------------
|
||||
|
||||
.. automodule:: sanic.request
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.response
|
||||
--------------
|
||||
sanic.response module
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.response
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.router
|
||||
------------
|
||||
sanic.router module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.router
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.server
|
||||
------------
|
||||
sanic.server module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.server
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.static module
|
||||
-------------------
|
||||
|
||||
sanic.views
|
||||
-----------
|
||||
.. 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
|
||||
---------------
|
||||
sanic.websocket module
|
||||
----------------------
|
||||
|
||||
.. automodule:: sanic.websocket
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.worker
|
||||
------------
|
||||
sanic.worker module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.worker
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
256
docs/sanic/blueprints.md
Normal file
256
docs/sanic/blueprints.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# Blueprints
|
||||
|
||||
Blueprints are objects that can be used for sub-routing within an application.
|
||||
Instead of adding routes to the application instance, blueprints define similar
|
||||
methods for adding routes, which are then registered with the application in a
|
||||
flexible and pluggable manner.
|
||||
|
||||
Blueprints are especially useful for larger applications, where your
|
||||
application logic can be broken down into several groups or areas of
|
||||
responsibility.
|
||||
|
||||
## My First Blueprint
|
||||
|
||||
The following shows a very simple blueprint that registers a handler-function at
|
||||
the root `/` of your application.
|
||||
|
||||
Suppose you save this file as `my_blueprint.py`, which can be imported into your
|
||||
main application later.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
from sanic import Blueprint
|
||||
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.route('/')
|
||||
async def bp_root(request):
|
||||
return json({'my': 'blueprint'})
|
||||
|
||||
```
|
||||
|
||||
## Registering blueprints
|
||||
|
||||
Blueprints must be registered with the application.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from my_blueprint import bp
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.blueprint(bp)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
|
||||
This will add the blueprint to the application and register any routes defined
|
||||
by that blueprint. In this example, the registered routes in the `app.router`
|
||||
will look like:
|
||||
|
||||
```python
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
|
||||
```
|
||||
|
||||
## Blueprint groups and nesting
|
||||
|
||||
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
|
||||
|
||||
```
|
||||
api/
|
||||
├──content/
|
||||
│ ├──authors.py
|
||||
│ ├──static.py
|
||||
│ └──__init__.py
|
||||
├──info.py
|
||||
└──__init__.py
|
||||
app.py
|
||||
```
|
||||
|
||||
Initialization of this app's blueprint hierarchy could go as follows:
|
||||
|
||||
```python
|
||||
# api/content/authors.py
|
||||
from sanic import Blueprint
|
||||
|
||||
authors = Blueprint('content_authors', url_prefix='/authors')
|
||||
```
|
||||
```python
|
||||
# api/content/static.py
|
||||
from sanic import Blueprint
|
||||
|
||||
static = Blueprint('content_static', url_prefix='/static')
|
||||
```
|
||||
```python
|
||||
# api/content/__init__.py
|
||||
from sanic import Blueprint
|
||||
|
||||
from .static import static
|
||||
from .authors import authors
|
||||
|
||||
content = Blueprint.group(static, authors, url_prefix='/content')
|
||||
```
|
||||
```python
|
||||
# api/info.py
|
||||
from sanic import Blueprint
|
||||
|
||||
info = Blueprint('info', url_prefix='/info')
|
||||
```
|
||||
```python
|
||||
# api/__init__.py
|
||||
from sanic import Blueprint
|
||||
|
||||
from .content import content
|
||||
from .info import info
|
||||
|
||||
api = Blueprint.group(content, info, url_prefix='/api')
|
||||
```
|
||||
|
||||
And registering these blueprints in `app.py` can now be done like so:
|
||||
|
||||
```python
|
||||
# app.py
|
||||
from sanic import Sanic
|
||||
|
||||
from .api import api
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.blueprint(api)
|
||||
```
|
||||
|
||||
## Using blueprints
|
||||
|
||||
Blueprints have much the same functionality as an application instance.
|
||||
|
||||
### WebSocket routes
|
||||
|
||||
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
|
||||
decorator or `bp.add_websocket_route` method.
|
||||
|
||||
### Middleware
|
||||
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
```python
|
||||
@bp.middleware
|
||||
async def print_on_request(request):
|
||||
print("I am a spy")
|
||||
|
||||
@bp.middleware('request')
|
||||
async def halt_request(request):
|
||||
return text('I halted the request')
|
||||
|
||||
@bp.middleware('response')
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
```
|
||||
|
||||
### Exceptions
|
||||
|
||||
Exceptions can be applied exclusively to blueprints globally.
|
||||
|
||||
```python
|
||||
@bp.exception(NotFound)
|
||||
def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
|
||||
### Static files
|
||||
|
||||
Static files can be served globally, under the blueprint prefix.
|
||||
|
||||
```python
|
||||
|
||||
# suppose bp.name == 'bp'
|
||||
|
||||
bp.static('/web/path', '/folder/to/serve')
|
||||
# also you can pass name parameter to it for url_for
|
||||
bp.static('/web/path', '/folder/to/server', name='uploads')
|
||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
|
||||
|
||||
```
|
||||
|
||||
## Start and stop
|
||||
|
||||
Blueprints can run functions during the start and stop process of the server.
|
||||
If running in multiprocessor mode (more than 1 worker), these are triggered
|
||||
after the workers fork.
|
||||
|
||||
Available events are:
|
||||
|
||||
- `before_server_start`: Executed before the server begins to accept connections
|
||||
- `after_server_start`: Executed after the server begins to accept connections
|
||||
- `before_server_stop`: Executed before the server stops accepting connections
|
||||
- `after_server_stop`: Executed after the server is stopped and all requests are complete
|
||||
|
||||
```python
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.listener('before_server_start')
|
||||
async def setup_connection(app, loop):
|
||||
global database
|
||||
database = mysql.connect(host='127.0.0.1'...)
|
||||
|
||||
@bp.listener('after_server_stop')
|
||||
async def close_connection(app, loop):
|
||||
await database.close()
|
||||
```
|
||||
|
||||
## Use-case: API versioning
|
||||
|
||||
Blueprints can be very useful for API versioning, where one blueprint may point
|
||||
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
||||
|
||||
When a blueprint is initialised, it can take an optional `url_prefix` argument,
|
||||
which will be prepended to all routes defined on the blueprint. This feature
|
||||
can be used to implement our API versioning scheme.
|
||||
|
||||
```python
|
||||
# blueprints.py
|
||||
from sanic.response import text
|
||||
from sanic import Blueprint
|
||||
|
||||
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
|
||||
blueprint_v2 = Blueprint('v2', url_prefix='/v2')
|
||||
|
||||
@blueprint_v1.route('/')
|
||||
async def api_v1_root(request):
|
||||
return text('Welcome to version 1 of our documentation')
|
||||
|
||||
@blueprint_v2.route('/')
|
||||
async def api_v2_root(request):
|
||||
return text('Welcome to version 2 of our documentation')
|
||||
```
|
||||
|
||||
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
|
||||
point to the individual blueprints, which allows the creation of *sub-sites*
|
||||
for each API version.
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from sanic import Sanic
|
||||
from blueprints import blueprint_v1, blueprint_v2
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.blueprint(blueprint_v1, url_prefix='/v1')
|
||||
app.blueprint(blueprint_v2, url_prefix='/v2')
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
|
||||
## URL Building with `url_for`
|
||||
|
||||
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
|
||||
takes the format `<blueprint_name>.<handler_name>`. For example:
|
||||
|
||||
```python
|
||||
@blueprint_v1.route('/')
|
||||
async def root(request):
|
||||
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@blueprint_v1.route('/post/<post_id>')
|
||||
async def post_handler(request, post_id):
|
||||
return text('Post {} in Blueprint V1'.format(post_id))
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
📜 Changelog
|
||||
============
|
||||
|
||||
.. include:: ../../CHANGELOG.rst
|
||||
166
docs/sanic/class_based_views.md
Normal file
166
docs/sanic/class_based_views.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Class-Based Views
|
||||
|
||||
Class-based views are simply classes which implement response behaviour to
|
||||
requests. They provide a way to compartmentalise handling of different HTTP
|
||||
request types at the same endpoint. Rather than defining and decorating three
|
||||
different handler functions, one for each of an endpoint's supported request
|
||||
type, the endpoint can be assigned a class-based view.
|
||||
|
||||
## Defining views
|
||||
|
||||
A class-based view should subclass `HTTPMethodView`. You can then implement
|
||||
class methods for every HTTP request type you want to support. If a request is
|
||||
received that has no defined method, a `405: Method not allowed` response will
|
||||
be generated.
|
||||
|
||||
To register a class-based view on an endpoint, the `app.add_route` method is
|
||||
used. The first argument should be the defined class with the method `as_view`
|
||||
invoked, and the second should be the URL endpoint.
|
||||
|
||||
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
|
||||
using all these methods would look like the following.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
|
||||
app.add_route(SimpleView.as_view(), '/')
|
||||
|
||||
```
|
||||
|
||||
You can also use `async` syntax.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
class SimpleAsyncView(HTTPMethodView):
|
||||
|
||||
async def get(self, request):
|
||||
return text('I am async get method')
|
||||
|
||||
app.add_route(SimpleAsyncView.as_view(), '/')
|
||||
|
||||
```
|
||||
|
||||
## URL parameters
|
||||
|
||||
If you need any URL parameters, as discussed in the routing guide, include them
|
||||
in the method definition.
|
||||
|
||||
```python
|
||||
class NameView(HTTPMethodView):
|
||||
|
||||
def get(self, request, name):
|
||||
return text('Hello {}'.format(name))
|
||||
|
||||
app.add_route(NameView.as_view(), '/<name>')
|
||||
```
|
||||
|
||||
## Decorators
|
||||
|
||||
If you want to add any decorators to the class, you can set the `decorators`
|
||||
class variable. These will be applied to the class when `as_view` is called.
|
||||
|
||||
```python
|
||||
class ViewWithDecorator(HTTPMethodView):
|
||||
decorators = [some_decorator_here]
|
||||
|
||||
def get(self, request, name):
|
||||
return text('Hello I have a decorator')
|
||||
|
||||
def post(self, request, name):
|
||||
return text("Hello I also have a decorator")
|
||||
|
||||
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||
```
|
||||
|
||||
But if you just want to decorate some functions and not all functions, you can do as follows:
|
||||
|
||||
```python
|
||||
class ViewWithSomeDecorator(HTTPMethodView):
|
||||
|
||||
@staticmethod
|
||||
@some_decorator_here
|
||||
def get(request, name):
|
||||
return text("Hello I have a decorator")
|
||||
|
||||
def post(self, request, name):
|
||||
return text("Hello I don't have any decorators")
|
||||
```
|
||||
|
||||
## URL Building
|
||||
|
||||
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
|
||||
that you will pass into `url_for`. For example:
|
||||
|
||||
```python
|
||||
@app.route('/')
|
||||
def index(request):
|
||||
url = app.url_for('SpecialClassView')
|
||||
return redirect(url)
|
||||
|
||||
|
||||
class SpecialClassView(HTTPMethodView):
|
||||
def get(self, request):
|
||||
return text('Hello from the Special Class View!')
|
||||
|
||||
|
||||
app.add_route(SpecialClassView.as_view(), '/special_class_view')
|
||||
```
|
||||
|
||||
|
||||
## Using CompositionView
|
||||
|
||||
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
|
||||
move handler functions outside of the view class.
|
||||
|
||||
Handler functions for each supported HTTP method are defined elsewhere in the
|
||||
source, and then added to the view using the `CompositionView.add` method. The
|
||||
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
|
||||
and the second is the handler function. The following example shows
|
||||
`CompositionView` usage with both an external handler function and an inline
|
||||
lambda:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import CompositionView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
def get_handler(request):
|
||||
return text('I am a get method')
|
||||
|
||||
view = CompositionView()
|
||||
view.add(['GET'], get_handler)
|
||||
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
|
||||
|
||||
# Use the new view to handle requests to the base URL
|
||||
app.add_route(view, '/')
|
||||
```
|
||||
|
||||
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
||||
121
docs/sanic/config.md
Normal file
121
docs/sanic/config.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Configuration
|
||||
|
||||
Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations.
|
||||
|
||||
## Basics
|
||||
|
||||
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.DB_NAME = 'appdb'
|
||||
app.config.DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
|
||||
|
||||
```
|
||||
db_settings = {
|
||||
'DB_HOST': 'localhost',
|
||||
'DB_NAME': 'appdb',
|
||||
'DB_USER': 'appuser'
|
||||
}
|
||||
app.config.update(db_settings)
|
||||
```
|
||||
|
||||
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
|
||||
|
||||
## Loading Configuration
|
||||
|
||||
There are several ways how to load configuration.
|
||||
|
||||
### From Environment Variables
|
||||
|
||||
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
|
||||
|
||||
```python
|
||||
app = Sanic(load_env='MYAPP_')
|
||||
```
|
||||
|
||||
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
|
||||
|
||||
```python
|
||||
app = Sanic(load_env=False)
|
||||
```
|
||||
|
||||
### From an Object
|
||||
|
||||
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
|
||||
|
||||
```
|
||||
import myapp.default_settings
|
||||
|
||||
app = Sanic('myapp')
|
||||
app.config.from_object(myapp.default_settings)
|
||||
```
|
||||
|
||||
You could use a class or any other object as well.
|
||||
|
||||
### From a File
|
||||
|
||||
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.from_envvar('MYAPP_SETTINGS')
|
||||
```
|
||||
|
||||
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
|
||||
|
||||
```
|
||||
$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
|
||||
INFO: Goin' Fast @ http://0.0.0.0:8000
|
||||
```
|
||||
|
||||
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
||||
|
||||
```
|
||||
# config_file
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'appdb'
|
||||
DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
## Builtin Configuration Values
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------- | --------- | ------------------------------------------------------ |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
||||
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long take to force close non-idle connection (sec) |
|
||||
| ACCESS_LOG | True | Disable or enable access log |
|
||||
|
||||
### The different Timeout variables:
|
||||
|
||||
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
|
||||
|
||||
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
|
||||
|
||||
### What is Keep Alive? And what does the Keep Alive Timeout value do?
|
||||
|
||||
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
|
||||
|
||||
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
|
||||
|
||||
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
|
||||
|
||||
For reference:
|
||||
```
|
||||
Apache httpd server default keepalive timeout = 5 seconds
|
||||
Nginx server default keepalive timeout = 75 seconds
|
||||
Nginx performance tuning guidelines uses keepalive = 15 seconds
|
||||
IE (5-9) client hard keepalive limit = 60 seconds
|
||||
Firefox client hard keepalive limit = 115 seconds
|
||||
Opera 11 client hard keepalive limit = 120 seconds
|
||||
Chrome 13+ client keepalive limit > 300+ seconds
|
||||
```
|
||||
62
docs/sanic/contributing.md
Normal file
62
docs/sanic/contributing.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
## Installation
|
||||
|
||||
To develop on sanic (and mainly to just run the tests) it is highly recommend to
|
||||
install from sources.
|
||||
|
||||
So assume you have already cloned the repo and are in the working directory with
|
||||
a virtual environment already set up, then run:
|
||||
|
||||
```bash
|
||||
python setup.py develop && pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
```bash
|
||||
tox
|
||||
```
|
||||
|
||||
See it's that simple!
|
||||
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
* All pull requests must pass unit tests
|
||||
* All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project
|
||||
* All pull requests must pass flake8 checks
|
||||
* If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
* If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
|
||||
## Documentation
|
||||
|
||||
Sanic's documentation is built
|
||||
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
|
||||
Markdown and can be found in the `docs` folder, while the module reference is
|
||||
automatically generated using `sphinx-apidoc`.
|
||||
|
||||
To generate the documentation from scratch:
|
||||
|
||||
```bash
|
||||
sphinx-apidoc -fo docs/_api/ sanic
|
||||
sphinx-build -b html docs docs/_build
|
||||
```
|
||||
|
||||
The HTML documentation will be created in the `docs/_build` folder.
|
||||
|
||||
## Warning
|
||||
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||
Sanic without significant gains in usability, security, or features may not be
|
||||
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||
idea, open an issue for discussion and help.
|
||||
@@ -1,4 +0,0 @@
|
||||
♥️ Contributing
|
||||
===============
|
||||
|
||||
.. include:: ../../CONTRIBUTING.rst
|
||||
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/
|
||||
72
docs/sanic/custom_protocol.md
Normal file
72
docs/sanic/custom_protocol.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Custom Protocols
|
||||
|
||||
*Note: this is advanced usage, and most readers will not need such functionality.*
|
||||
|
||||
You can change the behavior of Sanic's protocol by specifying a custom
|
||||
protocol, which should be a subclass
|
||||
of
|
||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||
This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method.
|
||||
|
||||
The constructor of the custom protocol class receives the following keyword
|
||||
arguments from Sanic.
|
||||
|
||||
- `loop`: an `asyncio`-compatible event loop.
|
||||
- `connections`: a `set` to store protocol objects. When Sanic receives
|
||||
`SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol
|
||||
objects stored in this set.
|
||||
- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When
|
||||
Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`.
|
||||
- `request_handler`: a coroutine that takes a `sanic.request.Request` object
|
||||
and a `response` callback as arguments.
|
||||
- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions
|
||||
are raised.
|
||||
- `request_timeout`: the number of seconds before a request times out.
|
||||
- `request_max_size`: an integer specifying the maximum size of a request, in bytes.
|
||||
|
||||
## Example
|
||||
|
||||
An error occurs in the default protocol if a handler function does not return
|
||||
an `HTTPResponse` object.
|
||||
|
||||
By overriding the `write_response` protocol method, if a handler returns a
|
||||
string it will be converted to an `HTTPResponse object`.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.server import HttpProtocol
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
class CustomHttpProtocol(HttpProtocol):
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal, connections, request_timeout, request_max_size):
|
||||
super().__init__(
|
||||
loop=loop, request_handler=request_handler,
|
||||
error_handler=error_handler, signal=signal,
|
||||
connections=connections, request_timeout=request_timeout,
|
||||
request_max_size=request_max_size)
|
||||
|
||||
def write_response(self, response):
|
||||
if isinstance(response, str):
|
||||
response = text(response)
|
||||
self.transport.write(
|
||||
response.output(self.request.version)
|
||||
)
|
||||
self.transport.close()
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def string(request):
|
||||
return 'string'
|
||||
|
||||
|
||||
@app.route('/1')
|
||||
async def response(request):
|
||||
return text('response')
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||
```
|
||||
53
docs/sanic/debug_mode.rst
Normal file
53
docs/sanic/debug_mode.rst
Normal file
@@ -0,0 +1,53 @@
|
||||
Debug Mode
|
||||
=============
|
||||
|
||||
When enabling Sanic's debug mode, Sanic will provide a more verbose logging output
|
||||
and by default will enable the Auto Reload feature.
|
||||
|
||||
.. warning::
|
||||
|
||||
Sanic's debug more will slow down the server's performance
|
||||
and is therefore advised to enable it only in development environments.
|
||||
|
||||
|
||||
Setting the debug mode
|
||||
----------------------
|
||||
|
||||
By setting the ``debug`` mode a more verbose output from Sanic will be outputed
|
||||
and the Automatic Reloader will be activated.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.route('/')
|
||||
async def hello_world(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
|
||||
|
||||
Manually setting auto reload
|
||||
----------------------------
|
||||
|
||||
Sanic offers a way to enable or disable the Automatic Reloader manually,
|
||||
the ``auto_reload`` argument will activate or deactivate the Automatic Reloader.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.route('/')
|
||||
async def hello_world(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, auto_reload=True)
|
||||
39
docs/sanic/decorators.md
Normal file
39
docs/sanic/decorators.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Handler Decorators
|
||||
|
||||
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
|
||||
|
||||
## Authorization Decorator
|
||||
|
||||
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
|
||||
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from sanic.response import json
|
||||
|
||||
def authorized():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
async def decorated_function(request, *args, **kwargs):
|
||||
# run some method that checks the request
|
||||
# for the client's authorization status
|
||||
is_authorized = check_request_for_authorization_status(request)
|
||||
|
||||
if is_authorized:
|
||||
# the user is authorized.
|
||||
# run the handler method and return the response
|
||||
response = await f(request, *args, **kwargs)
|
||||
return response
|
||||
else:
|
||||
# the user is not authorized.
|
||||
return json({'status': 'not_authorized'}, 403)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@authorized()
|
||||
async def test(request):
|
||||
return json({'status': 'authorized'})
|
||||
```
|
||||
|
||||
78
docs/sanic/deploying.md
Normal file
78
docs/sanic/deploying.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Deploying
|
||||
|
||||
Deploying Sanic is made simple by the inbuilt webserver. After defining an
|
||||
instance of `sanic.Sanic`, we can call the `run` method with the following
|
||||
keyword arguments:
|
||||
|
||||
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||
- `port` *(default `8000`)*: Port to host the server on.
|
||||
- `debug` *(default `False`)*: Enables debug output (slows server).
|
||||
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
||||
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
||||
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
||||
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is
|
||||
specified, Sanic creates its own event loop.
|
||||
- `protocol` *(default `HttpProtocol`)*: Subclass
|
||||
of
|
||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||
|
||||
## Workers
|
||||
|
||||
By default, Sanic listens in the main process using only one CPU core. To crank
|
||||
up the juice, just specify the number of workers in the `run` arguments.
|
||||
|
||||
```python
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
Sanic will automatically spin up multiple processes and route traffic between
|
||||
them. We recommend as many workers as you have available cores.
|
||||
|
||||
## Running via command
|
||||
|
||||
If you like using command line arguments, you can launch a Sanic server by
|
||||
executing the module. For example, if you initialized Sanic as `app` in a file
|
||||
named `server.py`, you could run the server like so:
|
||||
|
||||
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
|
||||
|
||||
With this way of running sanic, it is not necessary to invoke `app.run` in your
|
||||
Python file. If you do, make sure you wrap it so that it only executes when
|
||||
directly run by the interpreter.
|
||||
|
||||
```python
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
## Running via Gunicorn
|
||||
|
||||
[Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX.
|
||||
It’s a pre-fork worker model ported from Ruby’s Unicorn project.
|
||||
|
||||
In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
|
||||
for Gunicorn `worker-class` argument:
|
||||
|
||||
```
|
||||
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
||||
```
|
||||
|
||||
If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker
|
||||
after it has processed a given number of requests. This can be a convenient way to help limit the effects
|
||||
of the memory leak.
|
||||
|
||||
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
||||
|
||||
## Asynchronous support
|
||||
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
|
||||
However be advised that this method does not support using multiple processes, and is not the preferred way
|
||||
to run the app in general.
|
||||
|
||||
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
|
||||
|
||||
```python
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
loop.run_forever()
|
||||
```
|
||||
88
docs/sanic/exceptions.md
Normal file
88
docs/sanic/exceptions.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Exceptions
|
||||
|
||||
Exceptions can be thrown from within request handlers and will automatically be
|
||||
handled by Sanic. Exceptions take a message as their first argument, and can
|
||||
also take a status code to be passed back in the HTTP response.
|
||||
|
||||
## Throwing an exception
|
||||
|
||||
To throw an exception, simply `raise` the relevant exception from the
|
||||
`sanic.exceptions` module.
|
||||
|
||||
```python
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
@app.route('/killme')
|
||||
async def i_am_ready_to_die(request):
|
||||
raise ServerError("Something bad happened", status_code=500)
|
||||
```
|
||||
|
||||
You can also use the `abort` function with the appropriate status code:
|
||||
|
||||
```python
|
||||
from sanic.exceptions import abort
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/youshallnotpass')
|
||||
async def no_no(request):
|
||||
abort(401)
|
||||
# this won't happen
|
||||
text("OK")
|
||||
```
|
||||
|
||||
## Handling exceptions
|
||||
|
||||
To override Sanic's default handling of an exception, the `@app.exception`
|
||||
decorator is used. The decorator expects a list of exceptions to handle as
|
||||
arguments. You can pass `SanicException` to catch them all! The decorated
|
||||
exception handler function must take a `Request` and `Exception` object as
|
||||
arguments.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import NotFound
|
||||
|
||||
@app.exception(NotFound)
|
||||
async def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
|
||||
You can also add an exception handler as such:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
async def server_error_handler(request, exception):
|
||||
return text("Oops, server error", status=500)
|
||||
|
||||
app = Sanic()
|
||||
app.error_handler.add(Exception, server_error_handler)
|
||||
```
|
||||
|
||||
In some cases, you might want want to add some more error handling
|
||||
functionality to what is provided by default. In that case, you
|
||||
can subclass Sanic's default error handler as such:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
class CustomErrorHandler(ErrorHandler):
|
||||
def default(self, request, exception):
|
||||
''' handles errors that have no error handlers assigned '''
|
||||
# You custom error handling logic...
|
||||
return super().default(request, exception)
|
||||
|
||||
app = Sanic()
|
||||
app.error_handler = CustomErrorHandler()
|
||||
```
|
||||
|
||||
## Useful exceptions
|
||||
|
||||
Some of the most useful exceptions are presented below:
|
||||
|
||||
- `NotFound`: called when a suitable route for the request isn't found.
|
||||
- `ServerError`: called when something goes wrong inside the server. This
|
||||
usually occurs if there is an exception raised in user code.
|
||||
|
||||
See the `sanic.exceptions` module for the full list of exceptions to throw.
|
||||
36
docs/sanic/extensions.md
Normal file
36
docs/sanic/extensions.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
|
||||
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
|
||||
Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
|
||||
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
|
||||
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
|
||||
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
|
||||
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
|
||||
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
|
||||
`Babel` library
|
||||
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
|
||||
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
|
||||
- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
|
||||
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
|
||||
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with 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-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.
|
||||
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
|
||||
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
|
||||
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
|
||||
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
|
||||
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
|
||||
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
|
||||
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
|
||||
40
docs/sanic/getting_started.md
Normal file
40
docs/sanic/getting_started.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Getting Started
|
||||
|
||||
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||
syntax, so earlier versions of python won't work.
|
||||
|
||||
## 1. Install Sanic
|
||||
|
||||
```
|
||||
python3 -m pip install sanic
|
||||
```
|
||||
|
||||
## 2. Create a file called `main.py`
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## 3. Run the server
|
||||
|
||||
```
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## 4. Check your browser
|
||||
|
||||
Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
You now have a working Sanic server!
|
||||
25
docs/sanic/index.rst
Normal file
25
docs/sanic/index.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
Sanic
|
||||
=================================
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
||||
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
Sanic aspires to be simple
|
||||
---------------------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
100
docs/sanic/logging.md
Normal file
100
docs/sanic/logging.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Logging
|
||||
|
||||
|
||||
Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want to create a new configuration.
|
||||
|
||||
### Quick Start
|
||||
|
||||
A simple example using default settings would be like this:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.log import logger
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('test')
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
logger.info('Here is your log')
|
||||
return text('Hello World!')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, access_log=True)
|
||||
```
|
||||
|
||||
After the server is running, you can see some messages looks like:
|
||||
```
|
||||
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
|
||||
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
|
||||
```
|
||||
|
||||
You can send a request to server and it will print the log messages:
|
||||
```
|
||||
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
|
||||
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
|
||||
```
|
||||
|
||||
To use your own logging config, simply use `logging.config.dictConfig`, or
|
||||
pass `log_config` when you initialize `Sanic` app:
|
||||
|
||||
```python
|
||||
app = Sanic('test', log_config=LOGGING_CONFIG)
|
||||
```
|
||||
|
||||
And to close logging, simply assign access_log=False:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
app.run(access_log=False)
|
||||
```
|
||||
|
||||
This would skip calling logging functions when handling requests.
|
||||
And you could even do further in production to gain extra speed:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
# disable debug messages
|
||||
app.run(debug=False, access_log=False)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
|
||||
|
||||
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||
|
||||
- sanic.root:<br>
|
||||
Used to log internal messages.
|
||||
|
||||
- sanic.error:<br>
|
||||
Used to log error logs.
|
||||
|
||||
- sanic.access:<br>
|
||||
Used to log access logs.
|
||||
|
||||
#### Log format:
|
||||
|
||||
In addition to default parameters provided by python (asctime, levelname, message),
|
||||
Sanic provides additional parameters for access logger with:
|
||||
|
||||
- host (str)<br>
|
||||
request.ip
|
||||
|
||||
|
||||
- request (str)<br>
|
||||
request.method + " " + request.url
|
||||
|
||||
|
||||
- status (int)<br>
|
||||
response.status
|
||||
|
||||
|
||||
- byte (int)<br>
|
||||
len(response.body)
|
||||
|
||||
|
||||
The default access log format is
|
||||
```python
|
||||
%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d
|
||||
```
|
||||
147
docs/sanic/middleware.md
Normal file
147
docs/sanic/middleware.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Middleware And Listeners
|
||||
|
||||
Middleware are functions which are executed before or after requests to the
|
||||
server. They can be used to modify the *request to* or *response from*
|
||||
user-defined handler functions.
|
||||
|
||||
Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle.
|
||||
|
||||
## Middleware
|
||||
|
||||
There are two types of middleware: request and response. Both are declared
|
||||
using the `@app.middleware` decorator, with the decorator's parameter being a
|
||||
string representing its type: `'request'` or `'response'`.
|
||||
|
||||
* Request middleware receives only the `request` as argument.
|
||||
* Response middleware receives both the `request` and `response`.
|
||||
|
||||
The simplest middleware doesn't modify the request or response at all:
|
||||
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def print_on_request(request):
|
||||
print("I print when a request is received by the server")
|
||||
|
||||
@app.middleware('response')
|
||||
async def print_on_response(request, response):
|
||||
print("I print when a response is returned by the server")
|
||||
```
|
||||
|
||||
## Modifying the request or response
|
||||
|
||||
Middleware can modify the request or response parameter it is given, *as long
|
||||
as it does not return it*. The following example shows a practical use-case for
|
||||
this.
|
||||
|
||||
```
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.middleware('response')
|
||||
async def custom_banner(request, response):
|
||||
response.headers["Server"] = "Fake-Server"
|
||||
|
||||
@app.middleware('response')
|
||||
async def prevent_xss(request, response):
|
||||
response.headers["x-xss-protection"] = "1; mode=block"
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
The above code will apply the two middleware in order. First, the middleware
|
||||
**custom_banner** will change the HTTP response header *Server* to
|
||||
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
|
||||
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
|
||||
are invoked *after* a user function returns a response.
|
||||
|
||||
## Responding early
|
||||
|
||||
If middleware returns a `HTTPResponse` object, the request will stop processing
|
||||
and the response will be returned. If this occurs to a request before the
|
||||
relevant user route handler is reached, the handler will never be called.
|
||||
Returning a response will also prevent any further middleware from running.
|
||||
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def halt_request(request):
|
||||
return text('I halted the request')
|
||||
|
||||
@app.middleware('response')
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
```
|
||||
|
||||
## Listeners
|
||||
|
||||
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
|
||||
|
||||
- `before_server_start`
|
||||
- `after_server_start`
|
||||
- `before_server_stop`
|
||||
- `after_server_stop`
|
||||
|
||||
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
@app.listener('before_server_start')
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def notify_server_started(app, loop):
|
||||
print('Server successfully started!')
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
async def notify_server_stopping(app, loop):
|
||||
print('Server shutting down!')
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
async def close_db(app, loop):
|
||||
await app.db.close()
|
||||
```
|
||||
|
||||
It's also possible to register a listener using the `register_listener` method.
|
||||
This may be useful if you define your listeners in another module besides
|
||||
the one you instantiate your app in.
|
||||
|
||||
```
|
||||
app = Sanic()
|
||||
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
|
||||
app.register_listener(setup_db, 'before_server_start')
|
||||
|
||||
```
|
||||
|
||||
If you want to schedule a background task to run after the loop has started,
|
||||
Sanic provides the `add_task` method to easily do so.
|
||||
|
||||
```
|
||||
async def notify_server_started_after_five_seconds():
|
||||
await asyncio.sleep(5)
|
||||
print('Server successfully started!')
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds())
|
||||
```
|
||||
|
||||
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
|
||||
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds)
|
||||
```
|
||||
|
||||
Or you can pass the app explicitly for the same effect:
|
||||
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds(app))
|
||||
`
|
||||
128
docs/sanic/request_data.md
Normal file
128
docs/sanic/request_data.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Request Data
|
||||
|
||||
When an endpoint receives a HTTP request, the route function is passed a
|
||||
`Request` object.
|
||||
|
||||
The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
- `json` (any) - JSON body
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "message": request.json })
|
||||
```
|
||||
|
||||
- `args` (dict) - Query string variables. A query string is the section of a
|
||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
||||
The request's `query_string` variable holds the unparsed string value.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||
```
|
||||
|
||||
- `raw_args` (dict) - On many cases you would need to access the url arguments in
|
||||
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
|
||||
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||
|
||||
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/files")
|
||||
def post_json(request):
|
||||
test_file = request.files.get('test')
|
||||
|
||||
file_parameters = {
|
||||
'body': test_file.body,
|
||||
'name': test_file.name,
|
||||
'type': test_file.type,
|
||||
}
|
||||
|
||||
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
||||
```
|
||||
|
||||
- `form` (dict) - Posted form variables.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||
```
|
||||
|
||||
- `body` (bytes) - Posted raw body. This property allows retrieval of the
|
||||
request's raw data, regardless of content type.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/users", methods=["POST",])
|
||||
def create_user(request):
|
||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||
```
|
||||
|
||||
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
||||
|
||||
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
- `port` (str) - Port address of the requester.
|
||||
|
||||
- `socket` (tuple) - (IP, port) of the requester.
|
||||
|
||||
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
from sanic import Blueprint
|
||||
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.route('/')
|
||||
async def bp_root(request):
|
||||
if request.app.config['DEBUG']:
|
||||
return json({'status': 'debug'})
|
||||
else:
|
||||
return json({'status': 'production'})
|
||||
|
||||
```
|
||||
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
|
||||
- `scheme`: The URL scheme associated with the request: `http` or `https`
|
||||
- `host`: The host associated with the request: `localhost:8080`
|
||||
- `path`: The path of the request: `/posts/1/`
|
||||
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
|
||||
- `uri_template`: Template for matching route handler: `/posts/<id>/`
|
||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
||||
The request properties which return a dictionary actually return a subclass of
|
||||
`dict` called `RequestParameters`. The key difference when using this object is
|
||||
the distinction between the `get` and `getlist` methods.
|
||||
|
||||
- `get(key, default=None)` operates as normal, except that when the value of
|
||||
the given key is a list, *only the first item is returned*.
|
||||
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
|
||||
|
||||
```python
|
||||
from sanic.request import RequestParameters
|
||||
|
||||
args = RequestParameters()
|
||||
args['titles'] = ['Post 1', 'Post 2']
|
||||
|
||||
args.get('titles') # => 'Post 1'
|
||||
|
||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||
```
|
||||
112
docs/sanic/response.md
Normal file
112
docs/sanic/response.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Response
|
||||
|
||||
Use functions in `sanic.response` module to create responses.
|
||||
|
||||
## Plain Text
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/text')
|
||||
def handle_request(request):
|
||||
return response.text('Hello world!')
|
||||
```
|
||||
|
||||
## HTML
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/html')
|
||||
def handle_request(request):
|
||||
return response.html('<p>Hello world!</p>')
|
||||
```
|
||||
|
||||
## JSON
|
||||
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/json')
|
||||
def handle_request(request):
|
||||
return response.json({'message': 'Hello world!'})
|
||||
```
|
||||
|
||||
## File
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/file')
|
||||
async def handle_request(request):
|
||||
return await response.file('/srv/www/whatever.png')
|
||||
```
|
||||
|
||||
## Streaming
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
@app.route("/streaming")
|
||||
async def index(request):
|
||||
async def streaming_fn(response):
|
||||
response.write('foo')
|
||||
response.write('bar')
|
||||
return response.stream(streaming_fn, content_type='text/plain')
|
||||
```
|
||||
|
||||
## File Streaming
|
||||
For large files, a combination of File and Streaming above
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
@app.route('/big_file.png')
|
||||
async def handle_request(request):
|
||||
return await response.file_stream('/srv/www/whatever.png')
|
||||
```
|
||||
|
||||
## Redirect
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/redirect')
|
||||
def handle_request(request):
|
||||
return response.redirect('/json')
|
||||
```
|
||||
|
||||
## Raw
|
||||
|
||||
Response without encoding the body
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/raw')
|
||||
def handle_request(request):
|
||||
return response.raw(b'raw data')
|
||||
```
|
||||
|
||||
## Modify headers or status
|
||||
|
||||
To modify headers or status code, pass the `headers` or `status` argument to those functions:
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/json')
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'Hello world!'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=200
|
||||
)
|
||||
```
|
||||
335
docs/sanic/routing.md
Normal file
335
docs/sanic/routing.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Routing
|
||||
|
||||
Routing allows the user to specify handler functions for different URL endpoints.
|
||||
|
||||
A basic route looks like the following, where `app` is an instance of the
|
||||
`Sanic` class:
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({ "hello": "world" })
|
||||
```
|
||||
|
||||
When the url `http://server.url/` is accessed (the base url of the server), the
|
||||
final `/` is matched by the router to the handler function, `test`, which then
|
||||
returns a JSON object.
|
||||
|
||||
Sanic handler functions must be defined using the `async def` syntax, as they
|
||||
are asynchronous functions.
|
||||
|
||||
## Request parameters
|
||||
|
||||
Sanic comes with a basic router that supports request parameters.
|
||||
|
||||
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
|
||||
Request parameters will be passed to the route handler functions as keyword
|
||||
arguments.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/tag/<tag>')
|
||||
async def tag_handler(request, tag):
|
||||
return text('Tag - {}'.format(tag))
|
||||
```
|
||||
|
||||
To specify a type for the parameter, add a `:type` after the parameter name,
|
||||
inside the quotes. If the parameter does not match the specified type, Sanic
|
||||
will throw a `NotFound` exception, resulting in a `404: Page not found` error
|
||||
on the URL.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/number/<integer_arg:int>')
|
||||
async def integer_handler(request, integer_arg):
|
||||
return text('Integer - {}'.format(integer_arg))
|
||||
|
||||
@app.route('/number/<number_arg:number>')
|
||||
async def number_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number_arg))
|
||||
|
||||
@app.route('/person/<name:[A-z]+>')
|
||||
async def person_handler(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
|
||||
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
|
||||
async def folder_handler(request, folder_id):
|
||||
return text('Folder - {}'.format(folder_id))
|
||||
|
||||
```
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be available for only GET requests to that URL.
|
||||
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||
which allows the handler function to work with any of the HTTP methods in the list.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/post', methods=['POST'])
|
||||
async def post_handler(request):
|
||||
return text('POST request - {}'.format(request.json))
|
||||
|
||||
@app.route('/get', methods=['GET'])
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
```
|
||||
|
||||
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
|
||||
|
||||
```python
|
||||
@app.route('/get', methods=['GET'], host='example.com')
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
# if the host header doesn't match example.com, this route will be used
|
||||
@app.route('/get', methods=['GET'])
|
||||
async def get_handler(request):
|
||||
return text('GET request in default - {}'.format(request.args))
|
||||
```
|
||||
|
||||
There are also shorthand method decorators:
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.post('/post')
|
||||
async def post_handler(request):
|
||||
return text('POST request - {}'.format(request.json))
|
||||
|
||||
@app.get('/get')
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
```
|
||||
## The `add_route` method
|
||||
|
||||
As we have seen, routes are often specified using the `@app.route` decorator.
|
||||
However, this decorator is really just a wrapper for the `app.add_route`
|
||||
method, which is used as follows:
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
# Define the handler functions
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
async def handler2(request, name):
|
||||
return text('Folder - {}'.format(name))
|
||||
|
||||
async def person_handler2(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
|
||||
# Add each handler function as a route
|
||||
app.add_route(handler1, '/test')
|
||||
app.add_route(handler2, '/folder/<name>')
|
||||
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
|
||||
```
|
||||
|
||||
## URL building with `url_for`
|
||||
|
||||
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
|
||||
|
||||
```python
|
||||
from sanic.response import redirect
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
# generate a URL for the endpoint `post_handler`
|
||||
url = app.url_for('post_handler', post_id=5)
|
||||
# the URL is `/posts/5`, redirect to it
|
||||
return redirect(url)
|
||||
|
||||
@app.route('/posts/<post_id>')
|
||||
async def post_handler(request, post_id):
|
||||
return text('Post - {}'.format(post_id))
|
||||
```
|
||||
|
||||
Other things to keep in mind when using `url_for`:
|
||||
|
||||
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
||||
# /posts/5?arg_one=one&arg_two=two
|
||||
```
|
||||
- Multivalue argument can be passed to `url_for`. For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
|
||||
# /posts/5?arg_one=one&arg_one=two
|
||||
```
|
||||
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
|
||||
# /posts/5?arg_one=one#anchor
|
||||
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
|
||||
# //server/posts/5?arg_one=one
|
||||
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
|
||||
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
|
||||
# http://server/posts/5?arg_one=one
|
||||
# when specifying _scheme, _external must be True
|
||||
|
||||
# you can pass all special arguments one time
|
||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
|
||||
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
|
||||
```
|
||||
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
|
||||
|
||||
## WebSocket routes
|
||||
|
||||
Routes for the WebSocket protocol can be defined with the `@app.websocket`
|
||||
decorator:
|
||||
|
||||
```python
|
||||
@app.websocket('/feed')
|
||||
async def feed(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
```
|
||||
|
||||
Alternatively, the `app.add_websocket_route` method can be used instead of the
|
||||
decorator:
|
||||
|
||||
```python
|
||||
async def feed(request, ws):
|
||||
pass
|
||||
|
||||
app.add_websocket_route(my_websocket_handler, '/feed')
|
||||
```
|
||||
|
||||
Handlers for a WebSocket route are passed the request as first argument, and a
|
||||
WebSocket protocol object as second argument. The protocol object has `send`
|
||||
and `recv` methods to send and receive data respectively.
|
||||
|
||||
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
|
||||
package by Aymeric Augustin.
|
||||
|
||||
|
||||
## About `strict_slashes`
|
||||
|
||||
You can make `routes` strict to trailing slash or not, it's configurable.
|
||||
|
||||
```python
|
||||
|
||||
# provide default strict_slashes value for all routes
|
||||
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
||||
|
||||
# you can also overwrite strict_slashes value for specific route
|
||||
@app.get('/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# It also works for blueprints
|
||||
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
|
||||
|
||||
@bp.get('/bp/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
```
|
||||
|
||||
## User defined route name
|
||||
|
||||
You can pass `name` to change the route name to avoid using the default name (`handler.__name__`).
|
||||
|
||||
```python
|
||||
|
||||
app = Sanic('test_named_route')
|
||||
|
||||
@app.get('/get', name='get_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# then you need use `app.url_for('get_handler')`
|
||||
# instead of # `app.url_for('handler')`
|
||||
|
||||
# It also works for blueprints
|
||||
bp = Blueprint('test_named_bp')
|
||||
|
||||
@bp.get('/bp/get', name='get_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
# then you need use `app.url_for('test_named_bp.get_handler')`
|
||||
# instead of `app.url_for('test_named_bp.handler')`
|
||||
|
||||
# different names can be used for same url with different methods
|
||||
|
||||
@app.get('/test', name='route_test')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/test', name='route_post')
|
||||
def handler2(request):
|
||||
return text('OK POST')
|
||||
|
||||
@app.put('/test', name='route_put')
|
||||
def handler3(request):
|
||||
return text('OK PUT')
|
||||
|
||||
# below url are the same, you can use any of them
|
||||
# '/test'
|
||||
app.url_for('route_test')
|
||||
# app.url_for('route_post')
|
||||
# app.url_for('route_put')
|
||||
|
||||
# for same handler name with different methods
|
||||
# you need specify the name (it's url_for issue)
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/post', name='post_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# then
|
||||
# app.url_for('handler') == '/get'
|
||||
# app.url_for('post_handler') == '/post'
|
||||
```
|
||||
|
||||
## Build URL for static files
|
||||
|
||||
You can use `url_for` for static file url building now.
|
||||
If it's for file directly, `filename` can be ignored.
|
||||
|
||||
```python
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/static', './static')
|
||||
app.static('/uploads', './uploads', name='uploads')
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
|
||||
bp = Blueprint('bp', url_prefix='bp')
|
||||
bp.static('/static', './static')
|
||||
bp.static('/uploads', './uploads', name='uploads')
|
||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
app.blueprint(bp)
|
||||
|
||||
# then build the url
|
||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
|
||||
app.url_for('static', name='best_png') == '/the_best.png'
|
||||
|
||||
# blueprint url building
|
||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
|
||||
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
|
||||
|
||||
```
|
||||
66
docs/sanic/sockets.rst
Normal file
66
docs/sanic/sockets.rst
Normal file
@@ -0,0 +1,66 @@
|
||||
Sockets
|
||||
=======
|
||||
|
||||
Sanic can use the python
|
||||
`socket module <https://docs.python.org/3/library/socket.html>`_ to accommodate
|
||||
non IPv4 sockets.
|
||||
|
||||
IPv6 example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
import socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
sock.bind(('::', 7777))
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test IPv6 ``curl -g -6 "http://[::1]:7777/"``
|
||||
|
||||
|
||||
UNIX socket example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import socket
|
||||
import os
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
server_socket = '/tmp/sanic.sock'
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.bind(server_socket)
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print('Exiting')
|
||||
os.unlink(server_socket)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test UNIX: ``curl -v --unix-socket /tmp/sanic.sock http://localhost/hello``
|
||||
20
docs/sanic/ssl.rst
Normal file
20
docs/sanic/ssl.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
SSL Example
|
||||
-----------
|
||||
|
||||
Optionally pass in an SSLContext:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import ssl
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile")
|
||||
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
|
||||
You can also pass in the locations of a certificate and key as a dictionary:
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
ssl = {'cert': "/path/to/cert", 'key': "/path/to/keyfile"}
|
||||
app.run(host="0.0.0.0", port=8443, ssl=ssl)
|
||||
83
docs/sanic/static_files.md
Normal file
83
docs/sanic/static_files.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Static Files
|
||||
|
||||
Static files and directories, such as an image file, are served by Sanic when
|
||||
registered with the `app.static()` method. The method takes an endpoint URL and a
|
||||
filename. The file specified will then be accessible via the given endpoint.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
# Serves files from the static folder to the URL /static
|
||||
app.static('/static', './static')
|
||||
# use url_for to build the url, name defaults to 'static' and can be ignored
|
||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||
|
||||
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
|
||||
# is requested
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
|
||||
# you can use url_for to build the static file url
|
||||
# you can ignore name and filename parameters if you don't define it
|
||||
app.url_for('static', name='best_png') == '/the_best.png'
|
||||
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
|
||||
|
||||
# you need define the name for other static files
|
||||
app.static('/another.png', '/home/ubuntu/another.png', name='another')
|
||||
app.url_for('static', name='another') == '/another.png'
|
||||
app.url_for('static', name='another', filename='any') == '/another.png'
|
||||
|
||||
# also, you can use static for blueprint
|
||||
bp = Blueprint('bp', url_prefix='/bp')
|
||||
bp.static('/static', './static')
|
||||
|
||||
# servers the file directly
|
||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
app.blueprint(bp)
|
||||
|
||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
> **Note:** Sanic does not provide directory index when you serve a static directory.
|
||||
|
||||
## Virtual Host
|
||||
|
||||
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.static('/static', './static')
|
||||
app.static('/example_static', './example_static', host='www.example.com')
|
||||
```
|
||||
|
||||
## Streaming Large File
|
||||
|
||||
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
|
||||
|
||||
Here is an example:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
|
||||
```
|
||||
|
||||
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
|
||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
||||
```
|
||||
106
docs/sanic/streaming.md
Normal file
106
docs/sanic/streaming.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Streaming
|
||||
|
||||
## Request Streaming
|
||||
|
||||
Sanic allows you to get request data by stream, as below. When the request ends, `request.stream.get()` returns `None`. Only post, put and patch decorator have stream argument.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import CompositionView
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.views import stream as stream_decorator
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import stream, text
|
||||
|
||||
bp = Blueprint('blueprint_request_stream')
|
||||
app = Sanic('request_stream')
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
@stream_decorator
|
||||
async def post(self, request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
return text(result)
|
||||
|
||||
|
||||
@app.post('/stream', stream=True)
|
||||
async def handler(request):
|
||||
async def streaming(response):
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
body = body.decode('utf-8').replace('1', 'A')
|
||||
await response.write(body)
|
||||
return stream(streaming)
|
||||
|
||||
|
||||
@bp.put('/bp_stream', stream=True)
|
||||
async def bp_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8').replace('1', 'A')
|
||||
return text(result)
|
||||
|
||||
|
||||
async def post_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
return text(result)
|
||||
|
||||
app.blueprint(bp)
|
||||
app.add_route(SimpleView.as_view(), '/method_view')
|
||||
view = CompositionView()
|
||||
view.add(['POST'], post_handler, stream=True)
|
||||
app.add_route(view, '/composition_view')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='127.0.0.1', port=8000)
|
||||
```
|
||||
|
||||
## Response Streaming
|
||||
|
||||
Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import stream
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
async def sample_streaming_fn(response):
|
||||
await response.write('foo,')
|
||||
await response.write('bar')
|
||||
|
||||
return stream(sample_streaming_fn, content_type='text/csv')
|
||||
```
|
||||
|
||||
This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides:
|
||||
|
||||
```python
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
async def stream_from_db(response):
|
||||
conn = await asyncpg.connect(database='test')
|
||||
async with conn.transaction():
|
||||
async for record in conn.cursor('SELECT generate_series(0, 10)'):
|
||||
await response.write(record[0])
|
||||
|
||||
return stream(stream_from_db)
|
||||
```
|
||||
127
docs/sanic/testing.md
Normal file
127
docs/sanic/testing.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Testing
|
||||
|
||||
Sanic endpoints can be tested locally using the `test_client` object, which
|
||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||
library.
|
||||
|
||||
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
||||
for you to run against your application. A simple example (using pytest) is like follows:
|
||||
|
||||
```python
|
||||
# Import the Sanic app, usually created with Sanic(__name__)
|
||||
from external_server import app
|
||||
|
||||
def test_index_returns_200():
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.status == 200
|
||||
|
||||
def test_index_put_not_allowed():
|
||||
request, response = app.test_client.put('/')
|
||||
assert response.status == 405
|
||||
```
|
||||
|
||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
||||
your test request is executed against your application, using `aiohttp`.
|
||||
|
||||
The `test_client` methods accept the following arguments and keyword arguments:
|
||||
|
||||
- `uri` *(default `'/'`)* A string representing the URI to test.
|
||||
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||
original request will be returned by the function. If set to `True`, the
|
||||
return value is a tuple of `(request, response)`, if `False` only the
|
||||
response is returned.
|
||||
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
|
||||
|
||||
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
|
||||
|
||||
For example, to supply data to a GET request, you would do the following:
|
||||
|
||||
```python
|
||||
def test_get_request_includes_data():
|
||||
params = {'key1': 'value1', 'key2': 'value2'}
|
||||
request, response = app.test_client.get('/', params=params)
|
||||
assert request.args.get('key1') == 'value1'
|
||||
```
|
||||
|
||||
And to supply data to a JSON POST request:
|
||||
|
||||
```python
|
||||
def test_post_json_request_includes_data():
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
request, response = app.test_client.post('/', data=json.dumps(data))
|
||||
assert request.json.get('key1') == 'value1'
|
||||
```
|
||||
|
||||
|
||||
More information about
|
||||
the available arguments to aiohttp can be found
|
||||
[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', version=1)
|
||||
def handle_request(request):
|
||||
return response.text('Hello world! Version 1')
|
||||
|
||||
@app.route('/text', version=2)
|
||||
def handle_request(request):
|
||||
return response.text('Hello world! Version 2')
|
||||
|
||||
app.run(port=80)
|
||||
```
|
||||
|
||||
Then with curl:
|
||||
|
||||
```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
|
||||
```
|
||||
52
docs/sanic/websocket.rst
Normal file
52
docs/sanic/websocket.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
WebSocket
|
||||
=========
|
||||
|
||||
Sanic supports websockets, to setup a WebSocket:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.websocket('/feed')
|
||||
async def feed(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, protocol=WebSocketProtocol)
|
||||
|
||||
|
||||
Alternatively, the ``app.add_websocket_route`` method can be used instead of the
|
||||
decorator:
|
||||
|
||||
.. code:: python
|
||||
|
||||
async def feed(request, ws):
|
||||
pass
|
||||
|
||||
app.add_websocket_route(feed, '/feed')
|
||||
|
||||
|
||||
Handlers for a WebSocket route are passed the request as first argument, and a
|
||||
WebSocket protocol object as second argument. The protocol object has ``send``
|
||||
and ``recv`` methods to send and receive data respectively.
|
||||
|
||||
|
||||
You could setup your own WebSocket configuration through ``app.config``, like
|
||||
|
||||
.. code:: python
|
||||
|
||||
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
|
||||
app.config.WEBSOCKET_MAX_QUEUE = 32
|
||||
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
|
||||
|
||||
Find more in ``Configuration`` section.
|
||||
21
environment.yml
Normal file
21
environment.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
name: py35
|
||||
dependencies:
|
||||
- openssl=1.0.2g=0
|
||||
- pip=8.1.1=py35_0
|
||||
- python=3.5.1=0
|
||||
- readline=6.2=2
|
||||
- setuptools=20.3=py35_0
|
||||
- sqlite=3.9.2=0
|
||||
- tk=8.5.18=0
|
||||
- wheel=0.29.0=py35_0
|
||||
- xz=5.0.5=1
|
||||
- zlib=1.2.8=0
|
||||
- pip:
|
||||
- uvloop>=0.5.3
|
||||
- httptools>=0.0.10
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- websockets>=6.0
|
||||
- sphinxcontrib-asyncio>=0.2.0
|
||||
- multidict>=4.0,<5.0
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
@@ -1,8 +1,11 @@
|
||||
FROM sanicframework/sanic:LTS
|
||||
FROM python:3.5
|
||||
MAINTAINER Channel Cat <channelcat@gmail.com>
|
||||
|
||||
RUN mkdir /srv
|
||||
COPY . /srv
|
||||
ADD . /code
|
||||
RUN pip3 install git+https://github.com/channelcat/sanic
|
||||
|
||||
WORKDIR /srv
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["sanic", "simple_server.app"]
|
||||
WORKDIR /code
|
||||
|
||||
CMD ["python", "simple_server.py"]
|
||||
@@ -1,30 +0,0 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from random import randint
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.middleware('request')
|
||||
def append_request(request):
|
||||
# Add new key with random value
|
||||
request['num'] = randint(0, 100)
|
||||
|
||||
|
||||
@app.get('/pop')
|
||||
def pop_handler(request):
|
||||
# Pop key from request object
|
||||
num = request.pop('num')
|
||||
return text(num)
|
||||
|
||||
|
||||
@app.get('/key_exist')
|
||||
def key_exist_handler(request):
|
||||
# Check the key is exist or not
|
||||
if 'num' in request:
|
||||
return text('num exist in request')
|
||||
|
||||
return text('num does not exist in reqeust')
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
@@ -13,26 +13,28 @@ def check_request_for_authorization_status(request):
|
||||
return flag
|
||||
|
||||
|
||||
def authorized(f):
|
||||
@wraps(f)
|
||||
async def decorated_function(request, *args, **kwargs):
|
||||
# run some method that checks the request
|
||||
# for the client's authorization status
|
||||
is_authorized = check_request_for_authorization_status(request)
|
||||
def authorized():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
async def decorated_function(request, *args, **kwargs):
|
||||
# run some method that checks the request
|
||||
# for the client's authorization status
|
||||
is_authorized = check_request_for_authorization_status(request)
|
||||
|
||||
if is_authorized:
|
||||
# the user is authorized.
|
||||
# run the handler method and return the response
|
||||
response = await f(request, *args, **kwargs)
|
||||
return response
|
||||
else:
|
||||
# the user is not authorized.
|
||||
return json({'status': 'not_authorized'}, 403)
|
||||
return decorated_function
|
||||
if is_authorized:
|
||||
# the user is authorized.
|
||||
# run the handler method and return the response
|
||||
response = await f(request, *args, **kwargs)
|
||||
return response
|
||||
else:
|
||||
# the user is not authorized.
|
||||
return json({'status': 'not_authorized'}, 403)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@authorized
|
||||
@authorized()
|
||||
async def test(request):
|
||||
return json({'status': 'authorized'})
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
from sanic import Sanic, Blueprint
|
||||
from sanic.response import text
|
||||
'''
|
||||
Demonstrates that blueprint request middleware are executed in the order they
|
||||
are added. And blueprint response middleware are executed in _reverse_ order.
|
||||
On a valid request, it should print "1 2 3 6 5 4" to terminal
|
||||
'''
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
bp = Blueprint("bp_"+__name__)
|
||||
|
||||
@bp.middleware('request')
|
||||
def request_middleware_1(request):
|
||||
print('1')
|
||||
|
||||
@bp.middleware('request')
|
||||
def request_middleware_2(request):
|
||||
print('2')
|
||||
|
||||
@bp.middleware('request')
|
||||
def request_middleware_3(request):
|
||||
print('3')
|
||||
|
||||
@bp.middleware('response')
|
||||
def resp_middleware_4(request, response):
|
||||
print('4')
|
||||
|
||||
@bp.middleware('response')
|
||||
def resp_middleware_5(request, response):
|
||||
print('5')
|
||||
|
||||
@bp.middleware('response')
|
||||
def resp_middleware_6(request, response):
|
||||
print('6')
|
||||
|
||||
@bp.route('/')
|
||||
def pop_handler(request):
|
||||
return text('hello world')
|
||||
|
||||
app.blueprint(bp, url_prefix='/bp')
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)
|
||||
@@ -2,38 +2,37 @@ from sanic import Blueprint, Sanic
|
||||
from sanic.response import file, json
|
||||
|
||||
app = Sanic(__name__)
|
||||
blueprint = Blueprint("name", url_prefix="/my_blueprint")
|
||||
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
|
||||
blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3")
|
||||
blueprint = Blueprint('name', url_prefix='/my_blueprint')
|
||||
blueprint2 = Blueprint('name2', url_prefix='/my_blueprint2')
|
||||
blueprint3 = Blueprint('name3', url_prefix='/my_blueprint3')
|
||||
|
||||
|
||||
@blueprint.route("/foo")
|
||||
@blueprint.route('/foo')
|
||||
async def foo(request):
|
||||
return json({"msg": "hi from blueprint"})
|
||||
return json({'msg': 'hi from blueprint'})
|
||||
|
||||
|
||||
@blueprint2.route("/foo")
|
||||
@blueprint2.route('/foo')
|
||||
async def foo2(request):
|
||||
return json({"msg": "hi from blueprint2"})
|
||||
return json({'msg': 'hi from blueprint2'})
|
||||
|
||||
|
||||
@blueprint3.route("/foo")
|
||||
@blueprint3.route('/foo')
|
||||
async def index(request):
|
||||
return await file("websocket.html")
|
||||
return await file('websocket.html')
|
||||
|
||||
|
||||
@app.websocket("/feed")
|
||||
@app.websocket('/feed')
|
||||
async def foo3(request, ws):
|
||||
while True:
|
||||
data = "hello!"
|
||||
print("Sending: " + data)
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print("Received: " + data)
|
||||
|
||||
print('Received: ' + data)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
app.blueprint(blueprint2)
|
||||
app.blueprint(blueprint3)
|
||||
|
||||
app.run(host="0.0.0.0", port=9999, debug=True)
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
from asyncio import sleep
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
app = Sanic(__name__, strict_slashes=True)
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request):
|
||||
return response.redirect("/sleep/3")
|
||||
|
||||
@app.get("/sleep/<t:number>")
|
||||
async def handler2(request, t=0.3):
|
||||
await sleep(t)
|
||||
return response.text(f"Slept {t:.1f} seconds.\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -1,51 +0,0 @@
|
||||
from sanic import Sanic, response, text
|
||||
|
||||
|
||||
HTTP_PORT = 9999
|
||||
HTTPS_PORT = 8888
|
||||
|
||||
http = Sanic("http")
|
||||
http.config.SERVER_NAME = f"localhost:{HTTP_PORT}"
|
||||
https = Sanic("https")
|
||||
https.config.SERVER_NAME = f"localhost:{HTTPS_PORT}"
|
||||
|
||||
|
||||
@https.get("/foo")
|
||||
def foo(request):
|
||||
return text("foo")
|
||||
|
||||
|
||||
@https.get("/bar")
|
||||
def bar(request):
|
||||
return text("bar")
|
||||
|
||||
|
||||
@http.get("/<path:path>")
|
||||
def proxy(request, path):
|
||||
url = request.app.url_for(
|
||||
"proxy",
|
||||
path=path,
|
||||
_server=https.config.SERVER_NAME,
|
||||
_external=True,
|
||||
_scheme="http",
|
||||
)
|
||||
return response.redirect(url)
|
||||
|
||||
|
||||
@https.listener("main_process_start")
|
||||
async def start(app, _):
|
||||
global http
|
||||
app.http_server = await http.create_server(
|
||||
port=HTTP_PORT, return_asyncio_server=True
|
||||
)
|
||||
app.http_server.after_start()
|
||||
|
||||
|
||||
@https.listener("main_process_stop")
|
||||
async def stop(app, _):
|
||||
app.http_server.before_stop()
|
||||
await app.http_server.close()
|
||||
app.http_server.after_stop()
|
||||
|
||||
|
||||
https.run(port=HTTPS_PORT, debug=True)
|
||||
@@ -1,75 +1,86 @@
|
||||
'''
|
||||
Based on example from https://github.com/Skyscanner/aiotask-context
|
||||
and `examples/{override_logging,run_async}.py`.
|
||||
|
||||
Needs https://github.com/Skyscanner/aiotask-context/tree/52efbc21e2e1def2d52abb9a8e951f3ce5e6f690 or newer
|
||||
|
||||
$ pip install git+https://github.com/Skyscanner/aiotask-context.git
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import logging
|
||||
from signal import signal, SIGINT
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
import uvloop
|
||||
import aiotask_context as context
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestIdFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
try:
|
||||
record.request_id = context.get("X-Request-ID")
|
||||
except ValueError:
|
||||
record.request_id = "n/a"
|
||||
record.request_id = context.get('X-Request-ID')
|
||||
return True
|
||||
|
||||
|
||||
LOG_SETTINGS = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "default",
|
||||
"filters": ["requestid"],
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'level': 'DEBUG',
|
||||
'formatter': 'default',
|
||||
'filters': ['requestid'],
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"requestid": {
|
||||
"()": RequestIdFilter,
|
||||
'filters': {
|
||||
'requestid': {
|
||||
'()': RequestIdFilter,
|
||||
},
|
||||
},
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "%(asctime)s %(levelname)s %(name)s:%(lineno)d %(request_id)s | %(message)s",
|
||||
'formatters': {
|
||||
'default': {
|
||||
'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)d %(request_id)s | %(message)s',
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"": {"level": "DEBUG", "handlers": ["console"], "propagate": True},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
'propagate': True
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app = Sanic(__name__, log_config=LOG_SETTINGS)
|
||||
|
||||
|
||||
@app.on_request
|
||||
@app.middleware('request')
|
||||
async def set_request_id(request):
|
||||
request_id = request.id
|
||||
request_id = request.headers.get('X-Request-ID') or str(uuid.uuid4())
|
||||
context.set("X-Request-ID", request_id)
|
||||
log.info(f"Setting {request.id=}")
|
||||
|
||||
|
||||
@app.on_response
|
||||
async def set_request_header(request, response):
|
||||
response.headers["X-Request-ID"] = request.id
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
log.debug("X-Request-ID: %s", context.get("X-Request-ID"))
|
||||
log.info("Hello from test!")
|
||||
log.debug('X-Request-ID: %s', context.get('X-Request-ID'))
|
||||
log.info('Hello from test!')
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.before_server_start
|
||||
def setup(app, loop):
|
||||
if __name__ == '__main__':
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.set_task_factory(context.task_factory)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(port=9999, debug=True)
|
||||
task = asyncio.ensure_future(server)
|
||||
try:
|
||||
loop.run_forever()
|
||||
except:
|
||||
loop.stop()
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import logging
|
||||
import socket
|
||||
from os import getenv
|
||||
from platform import node
|
||||
from uuid import getnode as get_mac
|
||||
|
||||
from logdna import LogDNAHandler
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.request import Request
|
||||
|
||||
log = logging.getLogger('logdna')
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def get_my_ip_address(remote_server="google.com"):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect((remote_server, 80))
|
||||
return s.getsockname()[0]
|
||||
|
||||
|
||||
def get_mac_address():
|
||||
h = iter(hex(get_mac())[2:].zfill(12))
|
||||
return ":".join(i + next(h) for i in h)
|
||||
|
||||
|
||||
logdna_options = {
|
||||
"app": __name__,
|
||||
"index_meta": True,
|
||||
"hostname": node(),
|
||||
"ip": get_my_ip_address(),
|
||||
"mac": get_mac_address()
|
||||
}
|
||||
|
||||
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
|
||||
|
||||
logdna = logging.getLogger(__name__)
|
||||
logdna.setLevel(logging.INFO)
|
||||
logdna.addHandler(logdna_handler)
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.middleware
|
||||
def log_request(request: Request):
|
||||
logdna.info("I was Here with a new Request to URL: {}".format(request.url))
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def default(request):
|
||||
return json({
|
||||
"response": "I was here"
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
@@ -1,23 +1,24 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
import logging
|
||||
|
||||
from sanic import Sanic, text
|
||||
|
||||
|
||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||
logging_format += "%(message)s"
|
||||
|
||||
logging.basicConfig(format=logging_format, level=logging.DEBUG)
|
||||
logging.basicConfig(
|
||||
format=logging_format,
|
||||
level=logging.DEBUG
|
||||
)
|
||||
log = logging.getLogger()
|
||||
|
||||
# Set logger to override default basicConfig
|
||||
app = Sanic("app")
|
||||
sanic = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@sanic.route("/")
|
||||
def test(request):
|
||||
log.info("received request; responding with 'hey'")
|
||||
return text("hey")
|
||||
return response.text("hey")
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
sanic.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -9,19 +9,15 @@ Run with xdist params:
|
||||
$ pytest examples/pytest_xdist.py -n 8 # 8 workers
|
||||
"""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_testing import SanicTestClient
|
||||
from sanic_testing.testing import PORT as PORT_BASE
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.testing import PORT as PORT_BASE, SanicTestClient
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_port(worker_id):
|
||||
m = re.search(r"[0-9]+", worker_id)
|
||||
m = re.search(r'[0-9]+', worker_id)
|
||||
if m:
|
||||
num_id = m.group(0)
|
||||
else:
|
||||
@@ -34,9 +30,9 @@ def test_port(worker_id):
|
||||
def app():
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
return text("OK")
|
||||
return text('OK')
|
||||
|
||||
return app
|
||||
|
||||
@@ -46,8 +42,8 @@ def client(app, test_port):
|
||||
return SanicTestClient(app, test_port)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("run_id", range(100))
|
||||
@pytest.mark.parametrize('run_id', range(100))
|
||||
def test_index(client, run_id):
|
||||
request, response = client._sanic_endpoint_test("get", "/")
|
||||
request, response = client._sanic_endpoint_test('get', '/')
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
assert response.text == 'OK'
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from os import getenv
|
||||
|
||||
from raygun4py.raygunprovider import RaygunSender
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
|
||||
class RaygunExceptionReporter(ErrorHandler):
|
||||
|
||||
def __init__(self, raygun_api_key=None):
|
||||
super().__init__()
|
||||
if raygun_api_key is None:
|
||||
raygun_api_key = getenv("RAYGUN_API_KEY")
|
||||
|
||||
self.sender = RaygunSender(raygun_api_key)
|
||||
|
||||
def default(self, request, exception):
|
||||
self.sender.send_exception(exception=exception)
|
||||
return super().default(request, exception)
|
||||
|
||||
|
||||
raygun_error_reporter = RaygunExceptionReporter()
|
||||
app = Sanic(__name__, error_handler=raygun_error_reporter)
|
||||
|
||||
|
||||
@app.route("/raise")
|
||||
async def test(request):
|
||||
raise SanicException('You Broke It!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
import rollbar
|
||||
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import SanicException
|
||||
from os import getenv
|
||||
|
||||
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
||||
|
||||
|
||||
class RollbarExceptionHandler(ErrorHandler):
|
||||
|
||||
def default(self, request, exception):
|
||||
rollbar.report_message(str(exception))
|
||||
return super().default(request, exception)
|
||||
|
||||
|
||||
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
|
||||
|
||||
|
||||
@app.route("/raise")
|
||||
def create_error(request):
|
||||
raise SanicException("I was here and I don't like where I am")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
"""
|
||||
1. Create a simple Sanic app
|
||||
0. Run with an ASGI server:
|
||||
$ uvicorn run_asgi:app
|
||||
or
|
||||
$ hypercorn run_asgi:app
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/text")
|
||||
def handler_text(request):
|
||||
return response.text("Hello")
|
||||
|
||||
|
||||
@app.route("/json")
|
||||
def handler_json(request):
|
||||
return response.json({"foo": "bar"})
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def handler_ws(request, ws):
|
||||
name = "<someone>"
|
||||
while True:
|
||||
data = f"Hello {name}"
|
||||
await ws.send(data)
|
||||
name = await ws.recv()
|
||||
|
||||
if not name:
|
||||
break
|
||||
|
||||
|
||||
@app.route("/file")
|
||||
async def handler_file(request):
|
||||
return await response.file(Path("../") / "setup.py")
|
||||
|
||||
|
||||
@app.route("/file_stream")
|
||||
async def handler_file_stream(request):
|
||||
return await response.file_stream(
|
||||
Path("../") / "setup.py", chunk_size=1024
|
||||
)
|
||||
|
||||
|
||||
@app.post("/stream", stream=True)
|
||||
async def handler_stream(request):
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
body = body.decode("utf-8").replace("1", "A")
|
||||
await response.write(body)
|
||||
return response.stream(body)
|
||||
|
||||
|
||||
@app.listener("before_server_start")
|
||||
async def listener_before_server_start(*args, **kwargs):
|
||||
print("before_server_start")
|
||||
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def listener_after_server_start(*args, **kwargs):
|
||||
print("after_server_start")
|
||||
|
||||
|
||||
@app.listener("before_server_stop")
|
||||
async def listener_before_server_stop(*args, **kwargs):
|
||||
print("before_server_stop")
|
||||
|
||||
|
||||
@app.listener("after_server_stop")
|
||||
async def listener_after_server_stop(*args, **kwargs):
|
||||
print("after_server_stop")
|
||||
|
||||
|
||||
@app.middleware("request")
|
||||
async def print_on_request(request):
|
||||
print("print_on_request")
|
||||
|
||||
|
||||
@app.middleware("response")
|
||||
async def print_on_response(request, response):
|
||||
print("print_on_response")
|
||||
@@ -12,7 +12,7 @@ async def test(request):
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from signal import signal, SIGINT
|
||||
import asyncio
|
||||
import uvloop
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def after_start_test(app, loop):
|
||||
print("Async Server Started!")
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||
loop = asyncio.get_event_loop()
|
||||
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
server = loop.run_until_complete(serv_task)
|
||||
server.after_start()
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt as e:
|
||||
loop.stop()
|
||||
finally:
|
||||
server.before_stop()
|
||||
|
||||
# Wait for server to close
|
||||
close_task = server.close()
|
||||
loop.run_until_complete(close_task)
|
||||
|
||||
# Complete all tasks on the loop
|
||||
for connection in server.connections:
|
||||
connection.close_if_idle()
|
||||
server.after_stop()
|
||||
@@ -1,35 +0,0 @@
|
||||
from os import getenv
|
||||
|
||||
from sentry_sdk import init as sentry_init
|
||||
from sentry_sdk.integrations.sanic import SanicIntegration
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
sentry_init(
|
||||
dsn=getenv("SENTRY_DSN"),
|
||||
integrations=[SanicIntegration()],
|
||||
)
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@app.route("/working")
|
||||
async def working_path(request):
|
||||
return json({
|
||||
"response": "Working API Response"
|
||||
})
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@app.route("/raise-error")
|
||||
async def raise_error(request):
|
||||
raise Exception("Testing Sentry Integration")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
@@ -1,35 +0,0 @@
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic(name="blue-print-group-version-example")
|
||||
|
||||
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
|
||||
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
|
||||
|
||||
bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
|
||||
|
||||
|
||||
@bp1.get("/name")
|
||||
async def bp1_name(request):
|
||||
"""This will expose an Endpoint GET /v1/sentient/robot/ultron/name"""
|
||||
return json({"name": "Ultron"})
|
||||
|
||||
|
||||
@bp2.get("/name")
|
||||
async def bp2_name(request):
|
||||
"""This will expose an Endpoint GET /v1/sentient/robot/vision/name"""
|
||||
return json({"name": "vision"})
|
||||
|
||||
|
||||
@bp2.get("/name", version=2)
|
||||
async def bp2_revised_name(request):
|
||||
"""This will expose an Endpoint GET /v2/sentient/robot/vision/name"""
|
||||
return json({"name": "new vision"})
|
||||
|
||||
|
||||
app.blueprint(bpg)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -1,45 +1,39 @@
|
||||
from sanic import Sanic, response
|
||||
from sanic import response
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
# Usage
|
||||
# curl -H "Host: example.com" localhost:8000
|
||||
# curl -H "Host: sub.example.com" localhost:8000
|
||||
# curl -H "Host: bp.example.com" localhost:8000/question
|
||||
# curl -H "Host: bp.example.com" localhost:8000/answer
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic()
|
||||
bp = Blueprint("bp", host="bp.example.com")
|
||||
|
||||
|
||||
@app.route(
|
||||
"/", host=["example.com", "somethingelse.com", "therestofyourdomains.com"]
|
||||
)
|
||||
async def hello_0(request):
|
||||
@app.route('/', host=["example.com",
|
||||
"somethingelse.com",
|
||||
"therestofyourdomains.com"])
|
||||
async def hello(request):
|
||||
return response.text("Some defaults")
|
||||
|
||||
|
||||
@app.route("/", host="sub.example.com")
|
||||
async def hello_1(request):
|
||||
@app.route('/', host="sub.example.com")
|
||||
async def hello(request):
|
||||
return response.text("42")
|
||||
|
||||
|
||||
@bp.route("/question")
|
||||
async def hello_2(request):
|
||||
async def hello(request):
|
||||
return response.text("What is the meaning of life?")
|
||||
|
||||
|
||||
@bp.route("/answer")
|
||||
async def hello_3(request):
|
||||
async def hello(request):
|
||||
return response.text("42")
|
||||
|
||||
|
||||
@app.get("/name")
|
||||
def name(request):
|
||||
return response.text(request.app.url_for("name", _external=True))
|
||||
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -1,9 +1,2 @@
|
||||
version: 2
|
||||
python:
|
||||
version: 3.8
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
system_packages: true
|
||||
conda:
|
||||
file: environment.yml
|
||||
13
requirements-dev.txt
Normal file
13
requirements-dev.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
aiofiles
|
||||
aiohttp>=2.3.0,<=3.2.1
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
coverage
|
||||
httptools>=0.0.10
|
||||
flake8
|
||||
pytest==3.3.2
|
||||
tox
|
||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
||||
gunicorn
|
||||
multidict>=4.0,<5.0
|
||||
4
requirements-docs.txt
Normal file
4
requirements-docs.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
recommonmark
|
||||
sphinxcontrib-asyncio
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
aiofiles
|
||||
httptools>=0.0.10
|
||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
||||
websockets>=6.0,<7.0
|
||||
multidict>=4.0,<5.0
|
||||
@@ -1,17 +1,7 @@
|
||||
from sanic.__version__ import __version__
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, html, json, text
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
"Sanic",
|
||||
"Blueprint",
|
||||
"HTTPResponse",
|
||||
"Request",
|
||||
"html",
|
||||
"json",
|
||||
"text",
|
||||
)
|
||||
__version__ = "18.12.0"
|
||||
|
||||
__all__ = ["Sanic", "Blueprint"]
|
||||
|
||||
@@ -1,132 +1,56 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sanic import __version__
|
||||
from sanic.app import Sanic
|
||||
from sanic.config import BASE_LOGO
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
class SanicArgumentParser(ArgumentParser):
|
||||
def add_bool_arguments(self, *args, **kwargs):
|
||||
group = self.add_mutually_exclusive_group()
|
||||
group.add_argument(*args, action="store_true", **kwargs)
|
||||
kwargs["help"] = "no " + kwargs["help"]
|
||||
group.add_argument(
|
||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = SanicArgumentParser(
|
||||
prog="sanic",
|
||||
description=BASE_LOGO,
|
||||
formatter_class=RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-H",
|
||||
"--host",
|
||||
dest="host",
|
||||
type=str,
|
||||
default="127.0.0.1",
|
||||
help="host address [default 127.0.0.1]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--port",
|
||||
dest="port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="port to serve on [default 8000]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--unix",
|
||||
dest="unix",
|
||||
type=str,
|
||||
default="",
|
||||
help="location of unix socket",
|
||||
)
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(prog="sanic")
|
||||
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
|
||||
parser.add_argument("--port", dest="port", type=int, default=8000)
|
||||
parser.add_argument(
|
||||
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--workers",
|
||||
dest="workers",
|
||||
type=int,
|
||||
default=1,
|
||||
help="number of worker processes [default 1]",
|
||||
)
|
||||
parser.add_argument("--workers", dest="workers", type=int, default=1)
|
||||
parser.add_argument("--debug", dest="debug", action="store_true")
|
||||
parser.add_bool_arguments(
|
||||
"--access-logs", dest="access_log", help="display access logs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"Sanic {__version__}",
|
||||
)
|
||||
parser.add_argument(
|
||||
"module", help="path to your Sanic app. Example: path.to.server:app"
|
||||
)
|
||||
parser.add_argument("module")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
module_path = os.path.abspath(os.getcwd())
|
||||
if module_path not in sys.path:
|
||||
sys.path.append(module_path)
|
||||
|
||||
if ":" in args.module:
|
||||
module_name, app_name = args.module.rsplit(":", 1)
|
||||
else:
|
||||
module_parts = args.module.split(".")
|
||||
module_name = ".".join(module_parts[:-1])
|
||||
app_name = module_parts[-1]
|
||||
module_parts = args.module.split(".")
|
||||
module_name = ".".join(module_parts[:-1])
|
||||
app_name = module_parts[-1]
|
||||
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
app_name = type(app).__name__
|
||||
|
||||
if not isinstance(app, Sanic):
|
||||
raise ValueError(
|
||||
f"Module is not a Sanic app, it is a {app_name}. "
|
||||
f"Perhaps you meant {args.module}.app?"
|
||||
"Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?".format(
|
||||
type(app).__name__, args.module
|
||||
)
|
||||
)
|
||||
if args.cert is not None or args.key is not None:
|
||||
ssl = {
|
||||
"cert": args.cert,
|
||||
"key": args.key,
|
||||
} # type: Optional[Dict[str, Any]]
|
||||
ssl = {"cert": args.cert, "key": args.key}
|
||||
else:
|
||||
ssl = None
|
||||
|
||||
app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
unix=args.unix,
|
||||
workers=args.workers,
|
||||
debug=args.debug,
|
||||
access_log=args.access_log,
|
||||
ssl=ssl,
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error(
|
||||
f"No module named {e.name} found.\n"
|
||||
f" Example File: project/sanic_server.py -> app\n"
|
||||
f" Example Module: project.sanic_server.app"
|
||||
"No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app".format(e.name)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception("Failed to run app")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
__version__ = "21.3.0"
|
||||
1575
sanic/app.py
1575
sanic/app.py
File diff suppressed because it is too large
Load Diff
209
sanic/asgi.py
209
sanic/asgi.py
@@ -1,209 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from inspect import isawaitable
|
||||
from typing import Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
import sanic.app # noqa
|
||||
|
||||
from sanic.compat import Header
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
||||
from sanic.request import Request
|
||||
from sanic.server import ConnInfo
|
||||
from sanic.websocket import WebSocketConnection
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self, asgi_app: "ASGIApp") -> None:
|
||||
self.asgi_app = asgi_app
|
||||
|
||||
if "before_server_start" in self.asgi_app.sanic_app.listeners:
|
||||
warnings.warn(
|
||||
'You have set a listener for "before_server_start" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as early as possible, but not before "
|
||||
"the ASGI server is started."
|
||||
)
|
||||
if "after_server_stop" in self.asgi_app.sanic_app.listeners:
|
||||
warnings.warn(
|
||||
'You have set a listener for "after_server_stop" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as late as possible, but not after "
|
||||
"the ASGI server is stopped."
|
||||
)
|
||||
|
||||
async def startup(self) -> None:
|
||||
"""
|
||||
Gather the listeners to fire on server start.
|
||||
Because we are using a third-party server and not Sanic server, we do
|
||||
not have access to fire anything BEFORE the server starts.
|
||||
Therefore, we fire before_server_start and after_server_start
|
||||
in sequence since the ASGI lifespan protocol only supports a single
|
||||
startup event.
|
||||
"""
|
||||
self.asgi_app.sanic_app.router.finalize()
|
||||
if self.asgi_app.sanic_app.signal_router.routes:
|
||||
self.asgi_app.sanic_app.signal_router.finalize()
|
||||
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||
"before_server_start", []
|
||||
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
|
||||
|
||||
for handler in listeners:
|
||||
response = handler(
|
||||
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||
)
|
||||
if response and isawaitable(response):
|
||||
await response
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
Gather the listeners to fire on server stop.
|
||||
Because we are using a third-party server and not Sanic server, we do
|
||||
not have access to fire anything AFTER the server stops.
|
||||
Therefore, we fire before_server_stop and after_server_stop
|
||||
in sequence since the ASGI lifespan protocol only supports a single
|
||||
shutdown event.
|
||||
"""
|
||||
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||
"before_server_stop", []
|
||||
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
|
||||
|
||||
for handler in listeners:
|
||||
response = handler(
|
||||
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||
)
|
||||
if response and isawaitable(response):
|
||||
await response
|
||||
|
||||
async def __call__(
|
||||
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||
) -> None:
|
||||
message = await receive()
|
||||
if message["type"] == "lifespan.startup":
|
||||
await self.startup()
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
|
||||
message = await receive()
|
||||
if message["type"] == "lifespan.shutdown":
|
||||
await self.shutdown()
|
||||
await send({"type": "lifespan.shutdown.complete"})
|
||||
|
||||
|
||||
class ASGIApp:
|
||||
sanic_app: "sanic.app.Sanic"
|
||||
request: Request
|
||||
transport: MockTransport
|
||||
lifespan: Lifespan
|
||||
ws: Optional[WebSocketConnection]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.ws = None
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
cls, sanic_app, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||
) -> "ASGIApp":
|
||||
instance = cls()
|
||||
instance.sanic_app = sanic_app
|
||||
instance.transport = MockTransport(scope, receive, send)
|
||||
instance.transport.loop = sanic_app.loop
|
||||
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
|
||||
|
||||
headers = Header(
|
||||
[
|
||||
(key.decode("latin-1"), value.decode("latin-1"))
|
||||
for key, value in scope.get("headers", [])
|
||||
]
|
||||
)
|
||||
instance.lifespan = Lifespan(instance)
|
||||
|
||||
if scope["type"] == "lifespan":
|
||||
await instance.lifespan(scope, receive, send)
|
||||
else:
|
||||
path = (
|
||||
scope["path"][1:]
|
||||
if scope["path"].startswith("/")
|
||||
else scope["path"]
|
||||
)
|
||||
url = "/".join([scope.get("root_path", ""), quote(path)])
|
||||
url_bytes = url.encode("latin-1")
|
||||
url_bytes += b"?" + scope["query_string"]
|
||||
|
||||
if scope["type"] == "http":
|
||||
version = scope["http_version"]
|
||||
method = scope["method"]
|
||||
elif scope["type"] == "websocket":
|
||||
version = "1.1"
|
||||
method = "GET"
|
||||
|
||||
instance.ws = instance.transport.create_websocket_connection(
|
||||
send, receive
|
||||
)
|
||||
await instance.ws.accept()
|
||||
else:
|
||||
raise ServerError("Received unknown ASGI scope")
|
||||
|
||||
request_class = sanic_app.request_class or Request
|
||||
instance.request = request_class(
|
||||
url_bytes,
|
||||
headers,
|
||||
version,
|
||||
method,
|
||||
instance.transport,
|
||||
sanic_app,
|
||||
)
|
||||
instance.request.stream = instance
|
||||
instance.request_body = True
|
||||
instance.request.conn_info = ConnInfo(instance.transport)
|
||||
|
||||
return instance
|
||||
|
||||
async def read(self) -> Optional[bytes]:
|
||||
"""
|
||||
Read and stream the body in chunks from an incoming ASGI message.
|
||||
"""
|
||||
message = await self.transport.receive()
|
||||
if not message.get("more_body", False):
|
||||
self.request_body = False
|
||||
return None
|
||||
return message.get("body", b"")
|
||||
|
||||
async def __aiter__(self):
|
||||
while self.request_body:
|
||||
data = await self.read()
|
||||
if data:
|
||||
yield data
|
||||
|
||||
def respond(self, response):
|
||||
response.stream, self.response = self, response
|
||||
return response
|
||||
|
||||
async def send(self, data, end_stream):
|
||||
if self.response:
|
||||
response, self.response = self.response, None
|
||||
await self.transport.send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": response.status,
|
||||
"headers": response.processed_headers,
|
||||
}
|
||||
)
|
||||
response_body = getattr(response, "body", None)
|
||||
if response_body:
|
||||
data = response_body + data if data else response_body
|
||||
await self.transport.send(
|
||||
{
|
||||
"type": "http.response.body",
|
||||
"body": data.encode() if hasattr(data, "encode") else data,
|
||||
"more_body": not end_stream,
|
||||
}
|
||||
)
|
||||
|
||||
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||
|
||||
async def __call__(self) -> None:
|
||||
"""
|
||||
Handle the incoming request.
|
||||
"""
|
||||
await self.sanic_app.handle_request(self.request)
|
||||
@@ -1,41 +0,0 @@
|
||||
from typing import Any, Tuple
|
||||
from warnings import warn
|
||||
|
||||
from sanic.mixins.exceptions import ExceptionMixin
|
||||
from sanic.mixins.listeners import ListenerMixin
|
||||
from sanic.mixins.middleware import MiddlewareMixin
|
||||
from sanic.mixins.routes import RouteMixin
|
||||
from sanic.mixins.signals import SignalMixin
|
||||
|
||||
|
||||
class BaseSanic(
|
||||
RouteMixin,
|
||||
MiddlewareMixin,
|
||||
ListenerMixin,
|
||||
ExceptionMixin,
|
||||
SignalMixin,
|
||||
):
|
||||
__fake_slots__: Tuple[str, ...]
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
for base in BaseSanic.__bases__:
|
||||
base.__init__(self, *args, **kwargs) # type: ignore
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.name}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'{self.__class__.__name__}(name="{self.name}")'
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
# This is a temporary compat layer so we can raise a warning until
|
||||
# setting attributes on the app instance can be removed and deprecated
|
||||
# with a proper implementation of __slots__
|
||||
if name not in self.__fake_slots__:
|
||||
warn(
|
||||
f"Setting variables on {self.__class__.__name__} instances is "
|
||||
"deprecated and will be removed in version 21.9. You should "
|
||||
f"change your {self.__class__.__name__} instance to use "
|
||||
f"instance.ctx.{name} instead."
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
@@ -1,223 +0,0 @@
|
||||
from collections.abc import MutableSequence
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import sanic
|
||||
|
||||
|
||||
class BlueprintGroup(MutableSequence):
|
||||
"""
|
||||
This class provides a mechanism to implement a Blueprint Group
|
||||
using the :meth:`~sanic.blueprints.Blueprint.group` method in
|
||||
:class:`~sanic.blueprints.Blueprint`. To avoid having to re-write
|
||||
some of the existing implementation, this class provides a custom
|
||||
iterator implementation that will let you use the object of this
|
||||
class as a list/tuple inside the existing implementation.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
bp1 = Blueprint('bp1', url_prefix='/bp1')
|
||||
bp2 = Blueprint('bp2', url_prefix='/bp2')
|
||||
|
||||
bp3 = Blueprint('bp3', url_prefix='/bp4')
|
||||
bp3 = Blueprint('bp3', url_prefix='/bp4')
|
||||
|
||||
bpg = BlueprintGroup(bp3, bp4, url_prefix="/api", version="v1")
|
||||
|
||||
@bp1.middleware('request')
|
||||
async def bp1_only_middleware(request):
|
||||
print('applied on Blueprint : bp1 Only')
|
||||
|
||||
@bp1.route('/')
|
||||
async def bp1_route(request):
|
||||
return text('bp1')
|
||||
|
||||
@bp2.route('/<param>')
|
||||
async def bp2_route(request, param):
|
||||
return text(param)
|
||||
|
||||
@bp3.route('/')
|
||||
async def bp1_route(request):
|
||||
return text('bp1')
|
||||
|
||||
@bp4.route('/<param>')
|
||||
async def bp2_route(request, param):
|
||||
return text(param)
|
||||
|
||||
group = Blueprint.group(bp1, bp2)
|
||||
|
||||
@group.middleware('request')
|
||||
async def group_middleware(request):
|
||||
print('common middleware applied for both bp1 and bp2')
|
||||
|
||||
# Register Blueprint group under the app
|
||||
app.blueprint(group)
|
||||
app.blueprint(bpg)
|
||||
"""
|
||||
|
||||
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")
|
||||
|
||||
def __init__(self, url_prefix=None, version=None, strict_slashes=None):
|
||||
"""
|
||||
Create a new Blueprint Group
|
||||
|
||||
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
|
||||
:param version: API Version for the blueprint group. This will be
|
||||
inherited by each of the Blueprint
|
||||
:param strict_slashes: URL Strict slash behavior indicator
|
||||
"""
|
||||
self._blueprints = []
|
||||
self._url_prefix = url_prefix
|
||||
self._version = version
|
||||
self._strict_slashes = strict_slashes
|
||||
|
||||
@property
|
||||
def url_prefix(self) -> str:
|
||||
"""
|
||||
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||
|
||||
:return: string with url prefix
|
||||
"""
|
||||
return self._url_prefix
|
||||
|
||||
@property
|
||||
def blueprints(self) -> List["sanic.Blueprint"]:
|
||||
"""
|
||||
Retrieve a list of all the available blueprints under this group.
|
||||
|
||||
:return: List of Blueprint instance
|
||||
"""
|
||||
return self._blueprints
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[Union[str, int, float]]:
|
||||
"""
|
||||
API Version for the Blueprint Group. This will be applied only in case
|
||||
if the Blueprint doesn't already have a version specified
|
||||
|
||||
:return: Version information
|
||||
"""
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def strict_slashes(self) -> Optional[bool]:
|
||||
"""
|
||||
URL Slash termination behavior configuration
|
||||
|
||||
:return: bool
|
||||
"""
|
||||
return self._strict_slashes
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Tun the class Blueprint Group into an Iterable item
|
||||
"""
|
||||
return iter(self._blueprints)
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
This method returns a blueprint inside the group specified by
|
||||
an index value. This will enable indexing, splice and slicing
|
||||
of the blueprint group like we can do with regular list/tuple.
|
||||
|
||||
This method is provided to ensure backward compatibility with
|
||||
any of the pre-existing usage that might break.
|
||||
|
||||
:param item: Index of the Blueprint item in the group
|
||||
:return: Blueprint object
|
||||
"""
|
||||
return self._blueprints[item]
|
||||
|
||||
def __setitem__(self, index, item) -> None:
|
||||
"""
|
||||
Abstract method implemented to turn the `BlueprintGroup` class
|
||||
into a list like object to support all the existing behavior.
|
||||
|
||||
This method is used to perform the list's indexed setter operation.
|
||||
|
||||
:param index: Index to use for inserting a new Blueprint item
|
||||
:param item: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints[index] = item
|
||||
|
||||
def __delitem__(self, index) -> None:
|
||||
"""
|
||||
Abstract method implemented to turn the `BlueprintGroup` class
|
||||
into a list like object to support all the existing behavior.
|
||||
|
||||
This method is used to delete an item from the list of blueprint
|
||||
groups like it can be done on a regular list with index.
|
||||
|
||||
:param index: Index to use for removing a new Blueprint item
|
||||
:return: None
|
||||
"""
|
||||
del self._blueprints[index]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Get the Length of the blueprint group object.
|
||||
|
||||
:return: Length of Blueprint group object
|
||||
"""
|
||||
return len(self._blueprints)
|
||||
|
||||
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
|
||||
"""
|
||||
Sanitize the Blueprint Entity to override the Version and strict slash
|
||||
behaviors as required.
|
||||
|
||||
:param bp: Sanic Blueprint entity Object
|
||||
:return: Modified Blueprint
|
||||
"""
|
||||
if self._url_prefix:
|
||||
merged_prefix = "/".join(
|
||||
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
|
||||
).rstrip("/")
|
||||
bp.url_prefix = f"/{merged_prefix}"
|
||||
for _attr in ["version", "strict_slashes"]:
|
||||
if getattr(bp, _attr) is None:
|
||||
setattr(bp, _attr, getattr(self, _attr))
|
||||
return bp
|
||||
|
||||
def append(self, value: "sanic.Blueprint") -> None:
|
||||
"""
|
||||
The Abstract class `MutableSequence` leverages this append method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
:param value: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints.append(self._sanitize_blueprint(bp=value))
|
||||
|
||||
def insert(self, index: int, item: "sanic.Blueprint") -> None:
|
||||
"""
|
||||
The Abstract class `MutableSequence` leverages this insert method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
|
||||
:param index: Index to use for removing a new Blueprint item
|
||||
:param item: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints.insert(index, self._sanitize_blueprint(item))
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
A decorator that can be used to implement a Middleware plugin to
|
||||
all of the Blueprints that belongs to this specific Blueprint Group.
|
||||
|
||||
In case of nested Blueprint Groups, the same middleware is applied
|
||||
across each of the Blueprints recursively.
|
||||
|
||||
:param args: Optional positional Parameters to be use middleware
|
||||
:param kwargs: Optional Keyword arg to use with Middleware
|
||||
:return: Partial function to apply the middleware
|
||||
"""
|
||||
|
||||
def register_middleware_for_blueprints(fn):
|
||||
for blueprint in self.blueprints:
|
||||
blueprint.middleware(fn, *args, **kwargs)
|
||||
|
||||
if args and callable(args[0]):
|
||||
fn = args[0]
|
||||
args = list(args)[1:]
|
||||
return register_middleware_for_blueprints(fn)
|
||||
return register_middleware_for_blueprints
|
||||
@@ -1,286 +1,422 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
import asyncio
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
|
||||
|
||||
from sanic_routing.exceptions import NotFound # type: ignore
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.base import BaseSanic
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
from sanic.models.handler_types import (
|
||||
ListenerType,
|
||||
MiddlewareType,
|
||||
RouteHandler,
|
||||
FutureRoute = namedtuple(
|
||||
"FutureRoute",
|
||||
[
|
||||
"handler",
|
||||
"uri",
|
||||
"methods",
|
||||
"host",
|
||||
"strict_slashes",
|
||||
"stream",
|
||||
"version",
|
||||
"name",
|
||||
],
|
||||
)
|
||||
FutureListener = namedtuple(
|
||||
"FutureListener", ["handler", "uri", "methods", "host"]
|
||||
)
|
||||
FutureMiddleware = namedtuple(
|
||||
"FutureMiddleware", ["middleware", "args", "kwargs"]
|
||||
)
|
||||
FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"])
|
||||
FutureStatic = namedtuple(
|
||||
"FutureStatic", ["uri", "file_or_directory", "args", "kwargs"]
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic # noqa
|
||||
|
||||
|
||||
class Blueprint(BaseSanic):
|
||||
"""
|
||||
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
||||
URLs that perform a specific set of tasks which can be identified by
|
||||
a unique name.
|
||||
|
||||
It is the main tool for grouping functionality and similar endpoints.
|
||||
|
||||
`See user guide re: blueprints
|
||||
<https://sanicframework.org/guide/best-practices/blueprints.html>`__
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param host: IP Address of FQDN for the sanic server to use.
|
||||
:param version: Blueprint Version
|
||||
:param strict_slashes: Enforce the API urls are requested with a
|
||||
training */*
|
||||
"""
|
||||
|
||||
__fake_slots__ = (
|
||||
"_apps",
|
||||
"_future_routes",
|
||||
"_future_statics",
|
||||
"_future_middleware",
|
||||
"_future_listeners",
|
||||
"_future_exceptions",
|
||||
"_future_signals",
|
||||
"ctx",
|
||||
"exceptions",
|
||||
"host",
|
||||
"listeners",
|
||||
"middlewares",
|
||||
"name",
|
||||
"routes",
|
||||
"statics",
|
||||
"strict_slashes",
|
||||
"url_prefix",
|
||||
"version",
|
||||
"websocket_routes",
|
||||
)
|
||||
|
||||
class Blueprint:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
url_prefix: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
name,
|
||||
url_prefix=None,
|
||||
host=None,
|
||||
version=None,
|
||||
strict_slashes=False,
|
||||
):
|
||||
super().__init__()
|
||||
"""Create a new blueprint
|
||||
|
||||
self._apps: Set[Sanic] = set()
|
||||
self.ctx = SimpleNamespace()
|
||||
self.exceptions: List[RouteHandler] = []
|
||||
self.host = host
|
||||
self.listeners: Dict[str, List[ListenerType]] = {}
|
||||
self.middlewares: List[MiddlewareType] = []
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param strict_slashes: strict to trailing slash
|
||||
"""
|
||||
self.name = name
|
||||
self.routes: List[Route] = []
|
||||
self.statics: List[RouteHandler] = []
|
||||
self.strict_slashes = strict_slashes
|
||||
self.url_prefix = url_prefix
|
||||
self.host = host
|
||||
|
||||
self.routes = []
|
||||
self.websocket_routes = []
|
||||
self.exceptions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.middlewares = []
|
||||
self.statics = []
|
||||
self.version = version
|
||||
self.websocket_routes: List[Route] = []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
args = ", ".join(
|
||||
[
|
||||
f'{attr}="{getattr(self, attr)}"'
|
||||
if isinstance(getattr(self, attr), str)
|
||||
else f"{attr}={getattr(self, attr)}"
|
||||
for attr in (
|
||||
"name",
|
||||
"url_prefix",
|
||||
"host",
|
||||
"version",
|
||||
"strict_slashes",
|
||||
)
|
||||
]
|
||||
)
|
||||
return f"Blueprint({args})"
|
||||
|
||||
@property
|
||||
def apps(self):
|
||||
if not self._apps:
|
||||
raise SanicException(
|
||||
f"{self} has not yet been registered to an app"
|
||||
)
|
||||
return self._apps
|
||||
|
||||
def route(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().route(*args, **kwargs)
|
||||
|
||||
def static(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().static(*args, **kwargs)
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().middleware(*args, **kwargs)
|
||||
|
||||
def listener(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().listener(*args, **kwargs)
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().exception(*args, **kwargs)
|
||||
|
||||
def signal(self, event: str, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().signal(event, *args, **kwargs)
|
||||
self.strict_slashes = strict_slashes
|
||||
|
||||
@staticmethod
|
||||
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
|
||||
"""
|
||||
Create a list of blueprints, optionally grouping them under a
|
||||
general URL prefix.
|
||||
def group(*blueprints, url_prefix=""):
|
||||
"""Create a list of blueprints, optionally
|
||||
grouping them under a general URL prefix.
|
||||
|
||||
:param blueprints: blueprints to be registered as a group
|
||||
:param url_prefix: URL route to be prepended to all sub-prefixes
|
||||
:param version: API Version to be used for Blueprint group
|
||||
:param strict_slashes: Indicate strict slash termination behavior
|
||||
for URL
|
||||
"""
|
||||
|
||||
def chain(nested) -> Iterable[Blueprint]:
|
||||
def chain(nested):
|
||||
"""itertools.chain() but leaves strings untouched"""
|
||||
for i in nested:
|
||||
if isinstance(i, (list, tuple)):
|
||||
yield from chain(i)
|
||||
elif isinstance(i, BlueprintGroup):
|
||||
yield from i.blueprints
|
||||
else:
|
||||
yield i
|
||||
|
||||
bps = BlueprintGroup(
|
||||
url_prefix=url_prefix,
|
||||
version=version,
|
||||
strict_slashes=strict_slashes,
|
||||
)
|
||||
bps = []
|
||||
for bp in chain(blueprints):
|
||||
if bp.url_prefix is None:
|
||||
bp.url_prefix = ""
|
||||
bp.url_prefix = url_prefix + bp.url_prefix
|
||||
bps.append(bp)
|
||||
return bps
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Register the blueprint to the sanic app.
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
||||
:param app: Instance of :class:`sanic.app.Sanic` class
|
||||
:param options: Options to be used while registering the
|
||||
blueprint into the app.
|
||||
*url_prefix* - URL Prefix to override the blueprint prefix
|
||||
"""
|
||||
|
||||
self._apps.add(app)
|
||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
||||
|
||||
routes = []
|
||||
middleware = []
|
||||
exception_handlers = []
|
||||
listeners = defaultdict(list)
|
||||
|
||||
# Routes
|
||||
for future in self._future_routes:
|
||||
for future in self.routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
# prefixed properly in the router
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
|
||||
strict_slashes = (
|
||||
self.strict_slashes
|
||||
if future.strict_slashes is None
|
||||
and self.strict_slashes is not None
|
||||
else future.strict_slashes
|
||||
)
|
||||
name = app._generate_name(future.name)
|
||||
version = future.version or self.version
|
||||
|
||||
apply_route = FutureRoute(
|
||||
future.handler,
|
||||
uri[1:] if uri.startswith("//") else uri,
|
||||
future.methods,
|
||||
future.host or self.host,
|
||||
strict_slashes,
|
||||
future.stream,
|
||||
future.version or self.version,
|
||||
name,
|
||||
future.ignore_body,
|
||||
future.websocket,
|
||||
future.subprotocols,
|
||||
future.unquote,
|
||||
future.static,
|
||||
)
|
||||
app.route(
|
||||
uri=uri[1:] if uri.startswith("//") else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
route = app._apply_route(apply_route)
|
||||
operation = (
|
||||
routes.extend if isinstance(route, list) else routes.append
|
||||
)
|
||||
operation(route)
|
||||
|
||||
# Static Files
|
||||
for future in self._future_statics:
|
||||
for future in self.websocket_routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
# prefixed properly in the router
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
apply_route = FutureStatic(uri, *future[1:])
|
||||
route = app._apply_static(apply_route)
|
||||
routes.append(route)
|
||||
|
||||
route_names = [route.name for route in routes if route]
|
||||
app.websocket(
|
||||
uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
if route_names:
|
||||
for future in self._future_middleware:
|
||||
middleware.append(app._apply_middleware(future, route_names))
|
||||
for future in self.middlewares:
|
||||
if future.args or future.kwargs:
|
||||
app.register_middleware(
|
||||
future.middleware, *future.args, **future.kwargs
|
||||
)
|
||||
else:
|
||||
app.register_middleware(future.middleware)
|
||||
|
||||
# Exceptions
|
||||
for future in self._future_exceptions:
|
||||
exception_handlers.append(app._apply_exception_handler(future))
|
||||
for future in self.exceptions:
|
||||
app.exception(*future.args, **future.kwargs)(future.handler)
|
||||
|
||||
# Static Files
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
|
||||
# Event listeners
|
||||
for listener in self._future_listeners:
|
||||
listeners[listener.event].append(app._apply_listener(listener))
|
||||
for event, listeners in self.listeners.items():
|
||||
for listener in listeners:
|
||||
app.listener(event)(listener)
|
||||
|
||||
for signal in self._future_signals:
|
||||
signal.condition.update({"blueprint": self.name})
|
||||
app._apply_signal(signal)
|
||||
def route(
|
||||
self,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
self.routes = [route for route in routes if isinstance(route, Route)]
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
# Deprecate these in 21.6
|
||||
self.websocket_routes = [
|
||||
route for route in self.routes if route.ctx.websocket
|
||||
]
|
||||
self.middlewares = middleware
|
||||
self.exceptions = exception_handlers
|
||||
self.listeners = dict(listeners)
|
||||
def decorator(handler):
|
||||
route = FutureRoute(
|
||||
handler,
|
||||
uri,
|
||||
methods,
|
||||
host,
|
||||
strict_slashes,
|
||||
stream,
|
||||
version,
|
||||
name,
|
||||
)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
|
||||
async def dispatch(self, *args, **kwargs):
|
||||
condition = kwargs.pop("condition", {})
|
||||
condition.update({"blueprint": self.name})
|
||||
kwargs["condition"] = condition
|
||||
await asyncio.gather(
|
||||
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
||||
return decorator
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
handler,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, "view_class"):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
if getattr(handler.view_class, method.lower(), None):
|
||||
methods.add(method)
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(
|
||||
uri=uri,
|
||||
methods=methods,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(
|
||||
handler, uri, [], host, strict_slashes, False, version, name
|
||||
)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(
|
||||
self, handler, uri, host=None, version=None, name=None
|
||||
):
|
||||
"""Create a blueprint websocket route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
"""Create a listener from a decorated function.
|
||||
|
||||
:param event: Event to listen to.
|
||||
"""
|
||||
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
return listener
|
||||
|
||||
return decorator
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""Create a blueprint middleware from a decorated function."""
|
||||
|
||||
def register_middleware(_middleware):
|
||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||
self.middlewares.append(future_middleware)
|
||||
return _middleware
|
||||
|
||||
# Detect which way this was called, @middleware or @middleware('AT')
|
||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
||||
middleware = args[0]
|
||||
args = []
|
||||
return register_middleware(middleware)
|
||||
else:
|
||||
return register_middleware
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""Create a blueprint exception from a decorated function."""
|
||||
|
||||
def decorator(handler):
|
||||
exception = FutureException(handler, args, kwargs)
|
||||
self.exceptions.append(exception)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""Create a blueprint static route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
name = kwargs.pop("name", "static")
|
||||
if not name.startswith(self.name + "."):
|
||||
name = "{}.{}".format(self.name, name)
|
||||
kwargs.update(name=name)
|
||||
|
||||
strict_slashes = kwargs.get("strict_slashes")
|
||||
if strict_slashes is None and self.strict_slashes is not None:
|
||||
kwargs.update(strict_slashes=self.strict_slashes)
|
||||
|
||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||
self.statics.append(static)
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["GET"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def event(self, event: str, timeout: Optional[Union[int, float]] = None):
|
||||
events = set()
|
||||
for app in self.apps:
|
||||
signal = app.signal_router.name_index.get(event)
|
||||
if not signal:
|
||||
raise NotFound("Could not find signal %s" % event)
|
||||
events.add(signal.ctx.event)
|
||||
|
||||
return asyncio.wait(
|
||||
[event.wait() for event in events],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
timeout=timeout,
|
||||
def post(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["POST"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def put(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["PUT"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def head(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["HEAD"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def options(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["OPTIONS"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def patch(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["PATCH"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def delete(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["DELETE"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
|
||||
from sys import argv
|
||||
|
||||
from multidict import CIMultiDict # type: ignore
|
||||
|
||||
|
||||
OS_IS_WINDOWS = os.name == "nt"
|
||||
|
||||
|
||||
class Header(CIMultiDict):
|
||||
"""
|
||||
Container used for both request and response headers. It is a subclass of
|
||||
`CIMultiDict
|
||||
<https://multidict.readthedocs.io/en/stable/multidict.html#cimultidictproxy>`_.
|
||||
|
||||
It allows for multiple values for a single key in keeping with the HTTP
|
||||
spec. Also, all keys are *case in-sensitive*.
|
||||
|
||||
Please checkout `the MultiDict documentation
|
||||
<https://multidict.readthedocs.io/en/stable/multidict.html#multidict>`_
|
||||
for more details about how to use the object. In general, it should work
|
||||
very similar to a regular dictionary.
|
||||
"""
|
||||
|
||||
def get_all(self, key: str):
|
||||
"""
|
||||
Convenience method mapped to ``getall()``.
|
||||
"""
|
||||
return self.getall(key, default=[])
|
||||
|
||||
|
||||
use_trio = argv[0].endswith("hypercorn") and "trio" in argv
|
||||
|
||||
if use_trio: # pragma: no cover
|
||||
import trio # type: ignore
|
||||
|
||||
def stat_async(path):
|
||||
return trio.Path(path).stat()
|
||||
|
||||
open_async = trio.open_file
|
||||
CancelledErrors = tuple([asyncio.CancelledError, trio.Cancelled])
|
||||
else:
|
||||
from aiofiles import open as aio_open # type: ignore
|
||||
from aiofiles.os import stat as stat_async # type: ignore # noqa: F401
|
||||
|
||||
async def open_async(file, mode="r", **kwargs):
|
||||
return aio_open(file, mode, **kwargs)
|
||||
|
||||
CancelledErrors = tuple([asyncio.CancelledError])
|
||||
|
||||
|
||||
def ctrlc_workaround_for_windows(app):
|
||||
async def stay_active(app):
|
||||
"""Asyncio wakeups to allow receiving SIGINT in Python"""
|
||||
while not die:
|
||||
# If someone else stopped the app, just exit
|
||||
if app.is_stopping:
|
||||
return
|
||||
# Windows Python blocks signal handlers while the event loop is
|
||||
# waiting for I/O. Frequent wakeups keep interrupts flowing.
|
||||
await asyncio.sleep(0.1)
|
||||
# Can't be called from signal handler, so call it from here
|
||||
app.stop()
|
||||
|
||||
def ctrlc_handler(sig, frame):
|
||||
nonlocal die
|
||||
if die:
|
||||
raise KeyboardInterrupt("Non-graceful Ctrl+C")
|
||||
die = True
|
||||
|
||||
die = False
|
||||
signal.signal(signal.SIGINT, ctrlc_handler)
|
||||
app.add_task(stay_active)
|
||||
229
sanic/config.py
229
sanic/config.py
@@ -1,54 +1,47 @@
|
||||
from inspect import isclass
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
import os
|
||||
import types
|
||||
|
||||
from .utils import load_module_from_file_location, str_to_bool
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
BASE_LOGO = """
|
||||
|
||||
Sanic
|
||||
Build Fast. Run Fast.
|
||||
|
||||
"""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
||||
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
|
||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||
"KEEP_ALIVE": True,
|
||||
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||
"WEBSOCKET_MAX_QUEUE": 32,
|
||||
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||
"WEBSOCKET_PING_INTERVAL": 20,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"ACCESS_LOG": True,
|
||||
"FORWARDED_SECRET": None,
|
||||
"REAL_IP_HEADER": None,
|
||||
"PROXIES_COUNT": None,
|
||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||
"REQUEST_ID_HEADER": "X-Request-ID",
|
||||
"FALLBACK_ERROR_FORMAT": "html",
|
||||
"REGISTER": True,
|
||||
}
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
|
||||
self.LOGO = BASE_LOGO
|
||||
|
||||
if keep_alive is not None:
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||
super().__init__(defaults or {})
|
||||
self.LOGO = """
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \\
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
"""
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
|
||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||
self.WEBSOCKET_MAX_QUEUE = 32
|
||||
self.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
|
||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||
self.ACCESS_LOG = True
|
||||
|
||||
if load_env:
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
@@ -58,26 +51,76 @@ class Config(dict):
|
||||
try:
|
||||
return self[attr]
|
||||
except KeyError as ke:
|
||||
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
||||
raise AttributeError("Config has no '{}'".format(ke.args[0]))
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
self[attr] = value
|
||||
|
||||
def from_envvar(self, variable_name):
|
||||
"""Load a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
config_file = os.environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError(
|
||||
"The environment variable %r is not set and "
|
||||
"thus configuration could not be loaded." % variable_name
|
||||
)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
"""Update the values in the config from a Python file.
|
||||
Only the uppercase variables in that module are stored in the config.
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec(
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = "Unable to load configuration file (%s)" % e.strerror
|
||||
raise
|
||||
except Exception as e:
|
||||
raise PyFileError(filename) from e
|
||||
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Update the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
package because the package might be installed system wide.
|
||||
|
||||
:param obj: an object holding the configuration
|
||||
"""
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present. This is called automatically when
|
||||
Sanic starts up to load environment variables into config.
|
||||
|
||||
It will automatically hyrdate the following types:
|
||||
|
||||
- ``int``
|
||||
- ``float``
|
||||
- ``bool``
|
||||
|
||||
Anything else will be imported as a ``str``.
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in environ.items():
|
||||
for k, v in os.environ.items():
|
||||
if k.startswith(prefix):
|
||||
_, config_key = k.split(prefix, 1)
|
||||
try:
|
||||
@@ -86,76 +129,4 @@ class Config(dict):
|
||||
try:
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = str_to_bool(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""
|
||||
Update app.config.
|
||||
|
||||
.. note::
|
||||
|
||||
Only upper case settings are considered
|
||||
|
||||
You can upload app config by providing path to py file
|
||||
holding settings.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# /some/py/file
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
config.update_config("${some}/py/file")
|
||||
|
||||
Yes you can put environment variable here, but they must be provided
|
||||
in format: ``${some_env_var}``, and mark that ``$some_env_var`` is
|
||||
treated as plain string.
|
||||
|
||||
You can upload app config by providing dict holding settings.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
d = {"A": 1, "B": 2}
|
||||
config.update_config(d)
|
||||
|
||||
You can upload app config by providing any object holding settings,
|
||||
but in such case config.__dict__ will be used as dict holding settings.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class C:
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
config.update_config(C)
|
||||
|
||||
`See user guide re: config
|
||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||
"""
|
||||
|
||||
if isinstance(config, (bytes, str, Path)):
|
||||
config = load_module_from_file_location(location=config)
|
||||
|
||||
if not isinstance(config, dict):
|
||||
cfg = {}
|
||||
if not isclass(config):
|
||||
cfg.update(
|
||||
{
|
||||
key: getattr(config, key)
|
||||
for key in config.__class__.__dict__.keys()
|
||||
}
|
||||
)
|
||||
|
||||
config = dict(config.__dict__)
|
||||
config.update(cfg)
|
||||
|
||||
config = dict(filter(lambda i: i[0].isupper(), config.items()))
|
||||
|
||||
self.update(config)
|
||||
|
||||
load = update_config
|
||||
self[config_key] = v
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import re
|
||||
import string
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
|
||||
DEFAULT_MAX_AGE = 0
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# SimpleCookie
|
||||
@@ -42,17 +37,16 @@ _is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
|
||||
|
||||
|
||||
class CookieJar(dict):
|
||||
"""
|
||||
CookieJar dynamically writes headers as cookies are added and removed
|
||||
"""CookieJar dynamically writes headers as cookies are added and removed
|
||||
It gets around the limitation of one header per name by using the
|
||||
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
||||
"""
|
||||
|
||||
def __init__(self, headers):
|
||||
super().__init__()
|
||||
self.headers: Dict[str, str] = headers
|
||||
self.cookie_headers: Dict[str, str] = {}
|
||||
self.header_key: str = "Set-Cookie"
|
||||
self.headers = headers
|
||||
self.cookie_headers = {}
|
||||
self.header_key = "Set-Cookie"
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# If this cookie doesn't exist, add it to the header keys
|
||||
@@ -109,33 +103,9 @@ class Cookie(dict):
|
||||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property")
|
||||
if value is not False:
|
||||
if key.lower() == "max-age":
|
||||
if not str(value).isdigit():
|
||||
raise ValueError("Cookie max-age must be an integer")
|
||||
elif key.lower() == "expires":
|
||||
if not isinstance(value, datetime):
|
||||
raise TypeError(
|
||||
"Cookie 'expires' property must be a datetime"
|
||||
)
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
"""
|
||||
Encode the cookie content in a specific type of encoding instructed
|
||||
by the developer. Leverages the :func:`str.encode` method provided
|
||||
by python.
|
||||
|
||||
This method can be used to encode and embed ``utf-8`` content into
|
||||
the cookies.
|
||||
|
||||
:param encoding: Encoding to be used with the cookie
|
||||
:return: Cookie encoded in a codec of choosing.
|
||||
:except: UnicodeEncodeError
|
||||
"""
|
||||
return str(self).encode(encoding)
|
||||
|
||||
def __str__(self):
|
||||
"""Format as a Set-Cookie header value."""
|
||||
output = ["%s=%s" % (self.key, _quote(self.value))]
|
||||
for key, value in self.items():
|
||||
if key == "max-age":
|
||||
@@ -144,13 +114,19 @@ class Cookie(dict):
|
||||
except TypeError:
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
elif key == "expires":
|
||||
output.append(
|
||||
"%s=%s"
|
||||
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
|
||||
)
|
||||
try:
|
||||
output.append(
|
||||
"%s=%s"
|
||||
% (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT"),
|
||||
)
|
||||
)
|
||||
except AttributeError:
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
elif key in self._flags and self[key]:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
|
||||
return "; ".join(output)
|
||||
return "; ".join(output).encode(encoding)
|
||||
|
||||
@@ -1,380 +0,0 @@
|
||||
"""
|
||||
Sanic `provides a pattern
|
||||
<https://sanicframework.org/guide/best-practices/exceptions.html#using-sanic-exceptions>`_
|
||||
for providing a response when an exception occurs. However, if you do no handle
|
||||
an exception, it will provide a fallback. There are three fallback types:
|
||||
|
||||
- HTML - *default*
|
||||
- Text
|
||||
- JSON
|
||||
|
||||
Setting ``app.config.FALLBACK_ERROR_FORMAT = "auto"`` will enable a switch that
|
||||
will attempt to provide an appropriate response format based upon the
|
||||
request type.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
from functools import partial
|
||||
from traceback import extract_tb
|
||||
|
||||
from sanic.exceptions import InvalidUsage, SanicException
|
||||
from sanic.helpers import STATUS_CODES
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, html, json, text
|
||||
|
||||
|
||||
try:
|
||||
from ujson import dumps
|
||||
|
||||
dumps = partial(dumps, escape_forward_slashes=False)
|
||||
except ImportError: # noqa
|
||||
from json import dumps # type: ignore
|
||||
|
||||
|
||||
FALLBACK_TEXT = (
|
||||
"The server encountered an internal error and "
|
||||
"cannot complete your request."
|
||||
)
|
||||
FALLBACK_STATUS = 500
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
"""
|
||||
Base class that all renderers must inherit from.
|
||||
"""
|
||||
|
||||
def __init__(self, request, exception, debug):
|
||||
self.request = request
|
||||
self.exception = exception
|
||||
self.debug = debug
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
if isinstance(self.exception, SanicException):
|
||||
return getattr(self.exception, "headers", {})
|
||||
return {}
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if isinstance(self.exception, SanicException):
|
||||
return getattr(self.exception, "status_code", FALLBACK_STATUS)
|
||||
return FALLBACK_STATUS
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
if self.debug or isinstance(self.exception, SanicException):
|
||||
return str(self.exception)
|
||||
return FALLBACK_TEXT
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
status_text = STATUS_CODES.get(self.status, b"Error Occurred").decode()
|
||||
return f"{self.status} — {status_text}"
|
||||
|
||||
def render(self) -> HTTPResponse:
|
||||
"""
|
||||
Outputs the exception as a :class:`HTTPResponse`.
|
||||
|
||||
:return: The formatted exception
|
||||
:rtype: str
|
||||
"""
|
||||
output = (
|
||||
self.full
|
||||
if self.debug and not getattr(self.exception, "quiet", False)
|
||||
else self.minimal
|
||||
)
|
||||
return output()
|
||||
|
||||
def minimal(self) -> HTTPResponse: # noqa
|
||||
"""
|
||||
Provide a formatted message that is meant to not show any sensitive
|
||||
data or details.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def full(self) -> HTTPResponse: # noqa
|
||||
"""
|
||||
Provide a formatted message that has all details and is mean to be used
|
||||
primarily for debugging and non-production environments.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTMLRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as HTML.
|
||||
|
||||
The default fallback type.
|
||||
"""
|
||||
|
||||
TRACEBACK_STYLE = """
|
||||
html { font-family: sans-serif }
|
||||
h2 { color: #888; }
|
||||
.tb-wrapper p { margin: 0 }
|
||||
.frame-border { margin: 1rem }
|
||||
.frame-line > * { padding: 0.3rem 0.6rem }
|
||||
.frame-line { margin-bottom: 0.3rem }
|
||||
.frame-code { font-size: 16px; padding-left: 4ch }
|
||||
.tb-wrapper { border: 1px solid #eee }
|
||||
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
|
||||
.frame-descriptor { background: #e2eafb; font-size: 14px }
|
||||
"""
|
||||
TRACEBACK_WRAPPER_HTML = (
|
||||
"<div class=tb-header>{exc_name}: {exc_value}</div>"
|
||||
"<div class=tb-wrapper>{frame_html}</div>"
|
||||
)
|
||||
TRACEBACK_BORDER = (
|
||||
"<div class=frame-border>"
|
||||
"The above exception was the direct cause of the following exception:"
|
||||
"</div>"
|
||||
)
|
||||
TRACEBACK_LINE_HTML = (
|
||||
"<div class=frame-line>"
|
||||
"<p class=frame-descriptor>"
|
||||
"File {0.filename}, line <i>{0.lineno}</i>, "
|
||||
"in <code><b>{0.name}</b></code>"
|
||||
"<p class=frame-code><code>{0.line}</code>"
|
||||
"</div>"
|
||||
)
|
||||
OUTPUT_HTML = (
|
||||
"<!DOCTYPE html><html lang=en>"
|
||||
"<meta charset=UTF-8><title>{title}</title>\n"
|
||||
"<style>{style}</style>\n"
|
||||
"<h1>{title}</h1><p>{text}\n"
|
||||
"{body}"
|
||||
)
|
||||
|
||||
def full(self) -> HTTPResponse:
|
||||
return html(
|
||||
self.OUTPUT_HTML.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
style=self.TRACEBACK_STYLE,
|
||||
body=self._generate_body(),
|
||||
),
|
||||
status=self.status,
|
||||
)
|
||||
|
||||
def minimal(self) -> HTTPResponse:
|
||||
return html(
|
||||
self.OUTPUT_HTML.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
style=self.TRACEBACK_STYLE,
|
||||
body="",
|
||||
),
|
||||
status=self.status,
|
||||
headers=self.headers,
|
||||
)
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return escape(super().text)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return escape(f"⚠️ {super().title}")
|
||||
|
||||
def _generate_body(self):
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
||||
appname = escape(self.request.app.name)
|
||||
name = escape(self.exception.__class__.__name__)
|
||||
value = escape(self.exception)
|
||||
path = escape(self.request.path)
|
||||
lines = [
|
||||
f"<h2>Traceback of {appname} (most recent call last):</h2>",
|
||||
f"{traceback_html}",
|
||||
"<div class=summary><p>",
|
||||
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
|
||||
"</div>",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_exc(self, exc):
|
||||
frames = extract_tb(exc.__traceback__)
|
||||
frame_html = "".join(
|
||||
self.TRACEBACK_LINE_HTML.format(frame) for frame in frames
|
||||
)
|
||||
return self.TRACEBACK_WRAPPER_HTML.format(
|
||||
exc_name=escape(exc.__class__.__name__),
|
||||
exc_value=escape(exc),
|
||||
frame_html=frame_html,
|
||||
)
|
||||
|
||||
|
||||
class TextRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as plain text.
|
||||
"""
|
||||
|
||||
OUTPUT_TEXT = "{title}\n{bar}\n{text}\n\n{body}"
|
||||
SPACER = " "
|
||||
|
||||
def full(self) -> HTTPResponse:
|
||||
return text(
|
||||
self.OUTPUT_TEXT.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
bar=("=" * len(self.title)),
|
||||
body=self._generate_body(),
|
||||
),
|
||||
status=self.status,
|
||||
)
|
||||
|
||||
def minimal(self) -> HTTPResponse:
|
||||
return text(
|
||||
self.OUTPUT_TEXT.format(
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
bar=("=" * len(self.title)),
|
||||
body="",
|
||||
),
|
||||
status=self.status,
|
||||
headers=self.headers,
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return f"⚠️ {super().title}"
|
||||
|
||||
def _generate_body(self):
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
lines = [
|
||||
f"{self.exception.__class__.__name__}: {self.exception} while "
|
||||
f"handling path {self.request.path}",
|
||||
f"Traceback of {self.request.app.name} (most recent call last):\n",
|
||||
]
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
return "\n".join(lines + exceptions[::-1])
|
||||
|
||||
def _format_exc(self, exc):
|
||||
frames = "\n\n".join(
|
||||
[
|
||||
f"{self.SPACER * 2}File {frame.filename}, "
|
||||
f"line {frame.lineno}, in "
|
||||
f"{frame.name}\n{self.SPACER * 2}{frame.line}"
|
||||
for frame in extract_tb(exc.__traceback__)
|
||||
]
|
||||
)
|
||||
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
|
||||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
"""
|
||||
Render an exception as JSON.
|
||||
"""
|
||||
|
||||
def full(self) -> HTTPResponse:
|
||||
output = self._generate_output(full=True)
|
||||
return json(output, status=self.status, dumps=dumps)
|
||||
|
||||
def minimal(self) -> HTTPResponse:
|
||||
output = self._generate_output(full=False)
|
||||
return json(output, status=self.status, dumps=dumps)
|
||||
|
||||
def _generate_output(self, *, full):
|
||||
output = {
|
||||
"description": self.title,
|
||||
"status": self.status,
|
||||
"message": self.text,
|
||||
}
|
||||
|
||||
if full:
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(
|
||||
{
|
||||
"type": exc_value.__class__.__name__,
|
||||
"exception": str(exc_value),
|
||||
"frames": [
|
||||
{
|
||||
"file": frame.filename,
|
||||
"line": frame.lineno,
|
||||
"name": frame.name,
|
||||
"src": frame.line,
|
||||
}
|
||||
for frame in extract_tb(exc_value.__traceback__)
|
||||
],
|
||||
}
|
||||
)
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
output["path"] = self.request.path
|
||||
output["args"] = self.request.args
|
||||
output["exceptions"] = exceptions[::-1]
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return STATUS_CODES.get(self.status, b"Error Occurred").decode()
|
||||
|
||||
|
||||
def escape(text):
|
||||
"""
|
||||
Minimal HTML escaping, not for attribute values (unlike html.escape).
|
||||
"""
|
||||
return f"{text}".replace("&", "&").replace("<", "<")
|
||||
|
||||
|
||||
RENDERERS_BY_CONFIG = {
|
||||
"html": HTMLRenderer,
|
||||
"json": JSONRenderer,
|
||||
"text": TextRenderer,
|
||||
}
|
||||
|
||||
RENDERERS_BY_CONTENT_TYPE = {
|
||||
"multipart/form-data": HTMLRenderer,
|
||||
"application/json": JSONRenderer,
|
||||
"text/plain": TextRenderer,
|
||||
}
|
||||
|
||||
|
||||
def exception_response(
|
||||
request: Request,
|
||||
exception: Exception,
|
||||
debug: bool,
|
||||
renderer: t.Type[t.Optional[BaseRenderer]] = None,
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Render a response for the default FALLBACK exception handler.
|
||||
"""
|
||||
|
||||
if not renderer:
|
||||
renderer = HTMLRenderer
|
||||
|
||||
if request:
|
||||
if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
|
||||
try:
|
||||
renderer = JSONRenderer if request.json else HTMLRenderer
|
||||
except InvalidUsage:
|
||||
renderer = HTMLRenderer
|
||||
|
||||
content_type, *_ = request.headers.get(
|
||||
"content-type", ""
|
||||
).split(";")
|
||||
renderer = RENDERERS_BY_CONTENT_TYPE.get(
|
||||
content_type, renderer
|
||||
)
|
||||
else:
|
||||
render_format = request.app.config.FALLBACK_ERROR_FORMAT
|
||||
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
|
||||
|
||||
renderer = t.cast(t.Type[BaseRenderer], renderer)
|
||||
return renderer(request, exception, debug).render()
|
||||
@@ -1,20 +1,133 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
|
||||
TRACEBACK_STYLE = """
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
h3 code {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.frame-line > * {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.frame-line {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.frame-code {
|
||||
font-size: 16px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.tb-wrapper {
|
||||
border: 1px solid #f3f3f3;
|
||||
}
|
||||
|
||||
.tb-header {
|
||||
background-color: #f3f3f3;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.tb-border {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.frame-descriptor {
|
||||
background-color: #e2eafb;
|
||||
}
|
||||
|
||||
.frame-descriptor {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_HTML = """
|
||||
<html>
|
||||
<head>
|
||||
{style}
|
||||
</head>
|
||||
<body>
|
||||
{inner_html}
|
||||
<div class="summary">
|
||||
<p>
|
||||
<b>{exc_name}: {exc_value}</b>
|
||||
while handling path <code>{path}</code>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_INNER_HTML = """
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
</div>
|
||||
"""
|
||||
|
||||
TRACEBACK_BORDER = """
|
||||
<div class="tb-border">
|
||||
<b><i>
|
||||
The above exception was the direct cause of the
|
||||
following exception:
|
||||
</i></b>
|
||||
</div>
|
||||
"""
|
||||
|
||||
TRACEBACK_LINE_HTML = """
|
||||
<div class="frame-line">
|
||||
<p class="frame-descriptor">
|
||||
File {0.filename}, line <i>{0.lineno}</i>,
|
||||
in <code><b>{0.name}</b></code>
|
||||
</p>
|
||||
<p class="frame-code"><code>{0.line}</code></p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
INTERNAL_SERVER_ERROR_HTML = """
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>
|
||||
The server encountered an internal error and cannot complete
|
||||
your request.
|
||||
</p>
|
||||
"""
|
||||
|
||||
|
||||
_sanic_exceptions = {}
|
||||
|
||||
|
||||
def add_status_code(code, quiet=None):
|
||||
def add_status_code(code):
|
||||
"""
|
||||
Decorator used for adding exceptions to :class:`SanicException`.
|
||||
Decorator used for adding exceptions to _sanic_exceptions.
|
||||
"""
|
||||
|
||||
def class_decorator(cls):
|
||||
cls.status_code = code
|
||||
if quiet or quiet is None and code != 500:
|
||||
cls.quiet = True
|
||||
_sanic_exceptions[code] = cls
|
||||
return cls
|
||||
|
||||
@@ -22,80 +135,51 @@ def add_status_code(code, quiet=None):
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
def __init__(self, message, status_code=None, quiet=None):
|
||||
def __init__(self, message, status_code=None):
|
||||
super().__init__(message)
|
||||
|
||||
if status_code is not None:
|
||||
self.status_code = status_code
|
||||
|
||||
# quiet=None/False/True with None meaning choose by status
|
||||
if quiet or quiet is None and status_code not in (None, 500):
|
||||
self.quiet = True
|
||||
|
||||
|
||||
@add_status_code(404)
|
||||
class NotFound(SanicException):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(400)
|
||||
class InvalidUsage(SanicException):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(405)
|
||||
class MethodNotSupported(SanicException):
|
||||
"""
|
||||
**Status**: 405 Method Not Allowed
|
||||
"""
|
||||
|
||||
def __init__(self, message, method, allowed_methods):
|
||||
super().__init__(message)
|
||||
self.headers = {"Allow": ", ".join(allowed_methods)}
|
||||
self.headers = dict()
|
||||
self.headers["Allow"] = ", ".join(allowed_methods)
|
||||
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
|
||||
self.headers["Content-Length"] = 0
|
||||
|
||||
|
||||
@add_status_code(500)
|
||||
class ServerError(SanicException):
|
||||
"""
|
||||
**Status**: 500 Internal Server Error
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(503)
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""
|
||||
**Status**: 503 Service Unavailable
|
||||
|
||||
The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state.
|
||||
"""
|
||||
"""The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class URLBuildError(ServerError):
|
||||
"""
|
||||
**Status**: 500 Internal Server Error
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""
|
||||
|
||||
def __init__(self, message, path, relative_url):
|
||||
super().__init__(message)
|
||||
self.path = path
|
||||
@@ -117,55 +201,29 @@ class RequestTimeout(SanicException):
|
||||
|
||||
@add_status_code(413)
|
||||
class PayloadTooLarge(SanicException):
|
||||
"""
|
||||
**Status**: 413 Payload Too Large
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HeaderNotFound(InvalidUsage):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(416)
|
||||
class ContentRangeError(SanicException):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
||||
|
||||
|
||||
@add_status_code(417)
|
||||
class HeaderExpectationFailed(SanicException):
|
||||
"""
|
||||
**Status**: 417 Expectation Failed
|
||||
"""
|
||||
|
||||
pass
|
||||
self.headers = {
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Range": "bytes */%s" % (content_range.total,),
|
||||
}
|
||||
|
||||
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
"""
|
||||
**Status**: 403 Forbidden
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRangeType(ContentRangeError):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -177,7 +235,7 @@ class PyFileError(Exception):
|
||||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
**Status**: 401 Unauthorized
|
||||
Unauthorized exception (401 HTTP status code).
|
||||
|
||||
:param message: Message describing the exception.
|
||||
:param status_code: HTTP Status code.
|
||||
@@ -219,31 +277,22 @@ class Unauthorized(SanicException):
|
||||
challenge = ", ".join(values)
|
||||
|
||||
self.headers = {
|
||||
"WWW-Authenticate": f"{scheme} {challenge}".rstrip()
|
||||
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
|
||||
}
|
||||
|
||||
|
||||
class LoadFileException(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSignal(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||
def abort(status_code, message=None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
message appropriate for the given status code, unless provided.
|
||||
|
||||
STATUS_CODES from sanic.helpers for the given status code.
|
||||
|
||||
:param status_code: The HTTP status code to return.
|
||||
:param message: The HTTP response body. Defaults to the messages in
|
||||
:param message: The HTTP response body. Defaults to the messages
|
||||
in response.py for the given status code.
|
||||
"""
|
||||
if message is None:
|
||||
msg: bytes = STATUS_CODES[status_code]
|
||||
message = STATUS_CODES.get(status_code)
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = msg.decode("utf8")
|
||||
message = message.decode("utf8")
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
raise sanic_exception(message=message, status_code=status_code)
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
from traceback import format_exc
|
||||
import sys
|
||||
|
||||
from traceback import extract_tb, format_exc
|
||||
|
||||
from sanic.errorpages import exception_response
|
||||
from sanic.exceptions import (
|
||||
INTERNAL_SERVER_ERROR_HTML,
|
||||
TRACEBACK_BORDER,
|
||||
TRACEBACK_LINE_HTML,
|
||||
TRACEBACK_STYLE,
|
||||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
)
|
||||
from sanic.log import logger
|
||||
from sanic.response import text
|
||||
from sanic.response import html, text
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
"""
|
||||
Provide :class:`sanic.app.Sanic` application with a mechanism to handle
|
||||
and process any and all uncaught exceptions in a way the application
|
||||
developer will set fit.
|
||||
|
||||
This error handling framework is built into the core that can be extended
|
||||
by the developers to perform a wide range of tasks from recording the error
|
||||
stats to reporting them to an external service that can be used for
|
||||
realtime alerting system.
|
||||
|
||||
"""
|
||||
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
@@ -32,35 +28,39 @@ class ErrorHandler:
|
||||
self.cached_handlers = {}
|
||||
self.debug = False
|
||||
|
||||
def _render_exception(self, exception):
|
||||
frames = extract_tb(exception.__traceback__)
|
||||
|
||||
frame_html = []
|
||||
for frame in frames:
|
||||
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||
|
||||
return TRACEBACK_WRAPPER_INNER_HTML.format(
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
frame_html="".join(frame_html),
|
||||
)
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(self._render_exception(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
return TRACEBACK_WRAPPER_HTML.format(
|
||||
style=TRACEBACK_STYLE,
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
|
||||
path=request.path,
|
||||
)
|
||||
|
||||
def add(self, exception, handler):
|
||||
"""
|
||||
Add a new exception handler to an already existing handler object.
|
||||
|
||||
:param exception: Type of exception that need to be handled
|
||||
:param handler: Reference to the method that will handle the exception
|
||||
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
:type handler: ``function``
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
"""
|
||||
Lookup the existing instance of :class:`ErrorHandler` and fetch the
|
||||
registered handler for a specific type of exception.
|
||||
|
||||
This method leverages a dict lookup to speedup the retrieval process.
|
||||
|
||||
:param exception: Type of exception
|
||||
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
|
||||
:return: Registered function if found ``None`` otherwise
|
||||
"""
|
||||
handler = self.cached_handlers.get(type(exception), self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
@@ -75,15 +75,9 @@ class ErrorHandler:
|
||||
"""Fetches and executes an exception handler and returns a response
|
||||
object
|
||||
|
||||
:param request: Instance of :class:`sanic.request.Request`
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
|
||||
:type request: :class:`sanic.request.Request`
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
|
||||
:return: Wrap the return value obtained from :func:`default`
|
||||
or registered handler for that type of exception.
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.lookup(exception)
|
||||
response = None
|
||||
@@ -115,51 +109,31 @@ class ErrorHandler:
|
||||
"""
|
||||
|
||||
def default(self, request, exception):
|
||||
"""
|
||||
Provide a default behavior for the objects of :class:`ErrorHandler`.
|
||||
If a developer chooses to extent the :class:`ErrorHandler` they can
|
||||
provide a custom implementation for this method to behave in a way
|
||||
they see fit.
|
||||
self.log(format_exc())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
|
||||
:param request: Incoming request
|
||||
:param exception: Exception object
|
||||
response_message = "Exception occurred while handling uri: %s"
|
||||
logger.exception(response_message, url)
|
||||
|
||||
:type request: :class:`sanic.request.Request`
|
||||
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||
:class:`Exception`
|
||||
:return:
|
||||
"""
|
||||
quiet = getattr(exception, "quiet", False)
|
||||
if quiet is False:
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
"Error: {}".format(exception),
|
||||
status=getattr(exception, "status_code", 500),
|
||||
headers=getattr(exception, "headers", dict()),
|
||||
)
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
self.log(format_exc())
|
||||
logger.exception("Exception occurred while handling uri: %s", url)
|
||||
|
||||
return exception_response(request, exception, self.debug)
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
||||
|
||||
class ContentRangeHandler:
|
||||
"""
|
||||
A mechanism to parse and process the incoming request headers to
|
||||
extract the content range information.
|
||||
|
||||
:param request: Incoming api request
|
||||
:param stats: Stats related to the content
|
||||
|
||||
:type request: :class:`sanic.request.Request`
|
||||
:type stats: :class:`posix.stat_result`
|
||||
|
||||
:ivar start: Content Range start
|
||||
:ivar end: Content Range end
|
||||
:ivar size: Length of the content
|
||||
:ivar total: Total size identified by the :class:`posix.stat_result`
|
||||
instance
|
||||
:ivar ContentRangeHandler.headers: Content range header ``dict``
|
||||
"""
|
||||
"""Class responsible for parsing request header"""
|
||||
|
||||
__slots__ = ("start", "end", "size", "total", "headers")
|
||||
|
||||
|
||||
196
sanic/headers.py
196
sanic/headers.py
@@ -1,196 +0,0 @@
|
||||
import re
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
|
||||
# TODO:
|
||||
# - the Options object should be a typed object to allow for less casting
|
||||
# across the application (in request.py for example)
|
||||
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
|
||||
HeaderBytesIterable = Iterable[Tuple[bytes, bytes]]
|
||||
Options = Dict[str, Union[int, str]] # key=value fields in various headers
|
||||
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
|
||||
|
||||
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
||||
_param = re.compile(fr";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
||||
_firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)')
|
||||
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
|
||||
_ipv6_re = re.compile(_ipv6)
|
||||
_host_re = re.compile(
|
||||
r"((?:\[" + _ipv6 + r"\])|[a-zA-Z0-9.\-]{1,253})(?::(\d{1,5}))?"
|
||||
)
|
||||
|
||||
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
|
||||
# curl all have different escaping, that we try to handle as well as possible,
|
||||
# even though no client espaces in a way that would allow perfect handling.
|
||||
|
||||
# For more information, consult ../tests/test_requests.py
|
||||
|
||||
|
||||
def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||
"""Parse content-type and content-disposition header values.
|
||||
|
||||
E.g. 'form-data; name=upload; filename=\"file.txt\"' to
|
||||
('form-data', {'name': 'upload', 'filename': 'file.txt'})
|
||||
|
||||
Mostly identical to cgi.parse_header and werkzeug.parse_options_header
|
||||
but runs faster and handles special characters better. Unescapes quotes.
|
||||
"""
|
||||
value = _firefox_quote_escape.sub("%22", value)
|
||||
pos = value.find(";")
|
||||
if pos == -1:
|
||||
options: Dict[str, Union[int, str]] = {}
|
||||
else:
|
||||
options = {
|
||||
m.group(1).lower(): m.group(2) or m.group(3).replace("%22", '"')
|
||||
for m in _param.finditer(value[pos:])
|
||||
}
|
||||
value = value[:pos]
|
||||
return value.strip().lower(), options
|
||||
|
||||
|
||||
# https://tools.ietf.org/html/rfc7230#section-3.2.6 and
|
||||
# https://tools.ietf.org/html/rfc7239#section-4
|
||||
# This regex is for *reversed* strings because that works much faster for
|
||||
# right-to-left matching than the other way around. Be wary that all things are
|
||||
# a bit backwards! _rparam matches forwarded pairs alike ";key=value"
|
||||
_rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII)
|
||||
|
||||
|
||||
def parse_forwarded(headers, config) -> Optional[Options]:
|
||||
"""Parse RFC 7239 Forwarded headers.
|
||||
The value of `by` or `secret` must match `config.FORWARDED_SECRET`
|
||||
:return: dict with keys and values, or None if nothing matched
|
||||
"""
|
||||
header = headers.getall("forwarded", None)
|
||||
secret = config.FORWARDED_SECRET
|
||||
if header is None or not secret:
|
||||
return None
|
||||
header = ",".join(header) # Join multiple header lines
|
||||
if secret not in header:
|
||||
return None
|
||||
# Loop over <separator><key>=<value> elements from right to left
|
||||
sep = pos = None
|
||||
options: List[Tuple[str, str]] = []
|
||||
found = False
|
||||
for m in _rparam.finditer(header[::-1]):
|
||||
# Start of new element? (on parser skips and non-semicolon right sep)
|
||||
if m.start() != pos or sep != ";":
|
||||
# Was the previous element (from right) what we wanted?
|
||||
if found:
|
||||
break
|
||||
# Clear values and parse as new element
|
||||
del options[:]
|
||||
pos = m.end()
|
||||
val_token, val_quoted, key, sep = m.groups()
|
||||
key = key.lower()[::-1]
|
||||
val = (val_token or val_quoted.replace('"\\', '"'))[::-1]
|
||||
options.append((key, val))
|
||||
if key in ("secret", "by") and val == secret:
|
||||
found = True
|
||||
# Check if we would return on next round, to avoid useless parse
|
||||
if found and sep != ";":
|
||||
break
|
||||
# If secret was found, return the matching options in left-to-right order
|
||||
return fwd_normalize(reversed(options)) if found else None
|
||||
|
||||
|
||||
def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||
"""Parse traditional proxy headers."""
|
||||
real_ip_header = config.REAL_IP_HEADER
|
||||
proxies_count = config.PROXIES_COUNT
|
||||
addr = real_ip_header and headers.get(real_ip_header)
|
||||
if not addr and proxies_count:
|
||||
assert proxies_count > 0
|
||||
try:
|
||||
# Combine, split and filter multiple headers' entries
|
||||
forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER)
|
||||
proxies = [
|
||||
p
|
||||
for p in (
|
||||
p.strip() for h in forwarded_for for p in h.split(",")
|
||||
)
|
||||
if p
|
||||
]
|
||||
addr = proxies[-proxies_count]
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
# No processing of other headers if no address is found
|
||||
if not addr:
|
||||
return None
|
||||
|
||||
def options():
|
||||
yield "for", addr
|
||||
for key, header in (
|
||||
("proto", "x-scheme"),
|
||||
("proto", "x-forwarded-proto"), # Overrides X-Scheme if present
|
||||
("host", "x-forwarded-host"),
|
||||
("port", "x-forwarded-port"),
|
||||
("path", "x-forwarded-path"),
|
||||
):
|
||||
yield key, headers.get(header)
|
||||
|
||||
return fwd_normalize(options())
|
||||
|
||||
|
||||
def fwd_normalize(fwd: OptionsIterable) -> Options:
|
||||
"""Normalize and convert values extracted from forwarded headers."""
|
||||
ret: Dict[str, Union[int, str]] = {}
|
||||
for key, val in fwd:
|
||||
if val is not None:
|
||||
try:
|
||||
if key in ("by", "for"):
|
||||
ret[key] = fwd_normalize_address(val)
|
||||
elif key in ("host", "proto"):
|
||||
ret[key] = val.lower()
|
||||
elif key == "port":
|
||||
ret[key] = int(val)
|
||||
elif key == "path":
|
||||
ret[key] = unquote(val)
|
||||
else:
|
||||
ret[key] = val
|
||||
except ValueError:
|
||||
pass
|
||||
return ret
|
||||
|
||||
|
||||
def fwd_normalize_address(addr: str) -> str:
|
||||
"""Normalize address fields of proxy headers."""
|
||||
if addr == "unknown":
|
||||
raise ValueError() # omit unknown value identifiers
|
||||
if addr.startswith("_"):
|
||||
return addr # do not lower-case obfuscated strings
|
||||
if _ipv6_re.fullmatch(addr):
|
||||
addr = f"[{addr}]" # bracket IPv6
|
||||
return addr.lower()
|
||||
|
||||
|
||||
def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
|
||||
"""Split host:port into hostname and port.
|
||||
:return: None in place of missing elements
|
||||
"""
|
||||
m = _host_re.fullmatch(host)
|
||||
if not m:
|
||||
return None, None
|
||||
host, port = m.groups()
|
||||
return host.lower(), int(port) if port is not None else None
|
||||
|
||||
|
||||
_HTTP1_STATUSLINES = [
|
||||
b"HTTP/1.1 %d %b\r\n" % (status, STATUS_CODES.get(status, b"UNKNOWN"))
|
||||
for status in range(1000)
|
||||
]
|
||||
|
||||
|
||||
def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
||||
"""Format a HTTP/1.1 response header."""
|
||||
# Note: benchmarks show that here bytes concat is faster than bytearray,
|
||||
# b"".join() or %-formatting. %timeit any changes you make.
|
||||
ret = _HTTP1_STATUSLINES[status]
|
||||
for h in headers:
|
||||
ret += b"%b: %b\r\n" % h
|
||||
ret += b"\r\n"
|
||||
return ret
|
||||
@@ -1,15 +1,9 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
from importlib import import_module
|
||||
from inspect import ismodule
|
||||
from typing import Dict
|
||||
|
||||
|
||||
STATUS_CODES: Dict[int, bytes] = {
|
||||
STATUS_CODES = {
|
||||
100: b"Continue",
|
||||
101: b"Switching Protocols",
|
||||
102: b"Processing",
|
||||
103: b"Early Hints",
|
||||
200: b"OK",
|
||||
201: b"Created",
|
||||
202: b"Accepted",
|
||||
@@ -137,21 +131,3 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
||||
if not is_entity_header(header) or header.lower() in allowed
|
||||
}
|
||||
return headers
|
||||
|
||||
|
||||
def import_string(module_name, package=None):
|
||||
"""
|
||||
import a module or class by string path.
|
||||
|
||||
:module_name: str with path of module or path to import and
|
||||
instanciate a class
|
||||
:returns: a module object or one instance from class if
|
||||
module_name is a valid path to class
|
||||
|
||||
"""
|
||||
module, klass = module_name.rsplit(".", 1)
|
||||
module = import_module(module, package=package)
|
||||
obj = getattr(module, klass)
|
||||
if ismodule(obj):
|
||||
return obj
|
||||
return obj()
|
||||
|
||||
537
sanic/http.py
537
sanic/http.py
@@ -1,537 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse
|
||||
|
||||
from asyncio import CancelledError, sleep
|
||||
from enum import Enum
|
||||
|
||||
from sanic.compat import Header
|
||||
from sanic.exceptions import (
|
||||
HeaderExpectationFailed,
|
||||
InvalidUsage,
|
||||
PayloadTooLarge,
|
||||
ServerError,
|
||||
ServiceUnavailable,
|
||||
)
|
||||
from sanic.headers import format_http1_response
|
||||
from sanic.helpers import has_message_body
|
||||
from sanic.log import access_logger, logger
|
||||
|
||||
|
||||
class Stage(Enum):
|
||||
"""
|
||||
Enum for representing the stage of the request/response cycle
|
||||
|
||||
| ``IDLE`` Waiting for request
|
||||
| ``REQUEST`` Request headers being received
|
||||
| ``HANDLER`` Headers done, handler running
|
||||
| ``RESPONSE`` Response headers sent, body in progress
|
||||
| ``FAILED`` Unrecoverable state (error while sending response)
|
||||
|
|
||||
"""
|
||||
|
||||
IDLE = 0 # Waiting for request
|
||||
REQUEST = 1 # Request headers being received
|
||||
HANDLER = 3 # Headers done, handler running
|
||||
RESPONSE = 4 # Response headers sent, body in progress
|
||||
FAILED = 100 # Unrecoverable state (error while sending response)
|
||||
|
||||
|
||||
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
||||
|
||||
|
||||
class Http:
|
||||
"""
|
||||
Internal helper for managing the HTTP request/response cycle
|
||||
|
||||
:raises ServerError:
|
||||
:raises PayloadTooLarge:
|
||||
:raises Exception:
|
||||
:raises InvalidUsage:
|
||||
:raises HeaderExpectationFailed:
|
||||
:raises RuntimeError:
|
||||
:raises ServerError:
|
||||
:raises ServerError:
|
||||
:raises InvalidUsage:
|
||||
:raises InvalidUsage:
|
||||
:raises InvalidUsage:
|
||||
:raises PayloadTooLarge:
|
||||
:raises RuntimeError:
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
"_send",
|
||||
"_receive_more",
|
||||
"recv_buffer",
|
||||
"protocol",
|
||||
"expecting_continue",
|
||||
"stage",
|
||||
"keep_alive",
|
||||
"head_only",
|
||||
"request",
|
||||
"exception",
|
||||
"url",
|
||||
"request_body",
|
||||
"request_bytes",
|
||||
"request_bytes_left",
|
||||
"request_max_size",
|
||||
"response",
|
||||
"response_func",
|
||||
"response_bytes_left",
|
||||
"upgrade_websocket",
|
||||
]
|
||||
|
||||
def __init__(self, protocol):
|
||||
self._send = protocol.send
|
||||
self._receive_more = protocol.receive_more
|
||||
self.recv_buffer = protocol.recv_buffer
|
||||
self.protocol = protocol
|
||||
self.expecting_continue: bool = False
|
||||
self.stage: Stage = Stage.IDLE
|
||||
self.request_body = None
|
||||
self.request_bytes = None
|
||||
self.request_bytes_left = None
|
||||
self.request_max_size = protocol.request_max_size
|
||||
self.keep_alive = True
|
||||
self.head_only = None
|
||||
self.request: Request = None
|
||||
self.response: BaseHTTPResponse = None
|
||||
self.exception = None
|
||||
self.url = None
|
||||
self.upgrade_websocket = False
|
||||
|
||||
def __bool__(self):
|
||||
"""Test if request handling is in progress"""
|
||||
return self.stage in (Stage.HANDLER, Stage.RESPONSE)
|
||||
|
||||
async def http1(self):
|
||||
"""
|
||||
HTTP 1.1 connection handler
|
||||
"""
|
||||
while True: # As long as connection stays keep-alive
|
||||
try:
|
||||
# Receive and handle a request
|
||||
self.stage = Stage.REQUEST
|
||||
self.response_func = self.http1_response_header
|
||||
|
||||
await self.http1_request_header()
|
||||
|
||||
self.request.conn_info = self.protocol.conn_info
|
||||
await self.protocol.request_handler(self.request)
|
||||
|
||||
# Handler finished, response should've been sent
|
||||
if self.stage is Stage.HANDLER and not self.upgrade_websocket:
|
||||
raise ServerError("Handler produced no response")
|
||||
|
||||
if self.stage is Stage.RESPONSE:
|
||||
await self.response.send(end_stream=True)
|
||||
except CancelledError:
|
||||
# Write an appropriate response before exiting
|
||||
e = self.exception or ServiceUnavailable("Cancelled")
|
||||
self.exception = None
|
||||
self.keep_alive = False
|
||||
await self.error_response(e)
|
||||
except Exception as e:
|
||||
# Write an error response
|
||||
await self.error_response(e)
|
||||
|
||||
# Try to consume any remaining request body
|
||||
if self.request_body:
|
||||
if self.response and 200 <= self.response.status < 300:
|
||||
logger.error(f"{self.request} body not consumed.")
|
||||
|
||||
try:
|
||||
async for _ in self:
|
||||
pass
|
||||
except PayloadTooLarge:
|
||||
# We won't read the body and that may cause httpx and
|
||||
# tests to fail. This little delay allows clients to push
|
||||
# a small request into network buffers before we close the
|
||||
# socket, so that they are then able to read the response.
|
||||
await sleep(0.001)
|
||||
self.keep_alive = False
|
||||
|
||||
# Exit and disconnect if no more requests can be taken
|
||||
if self.stage is not Stage.IDLE or not self.keep_alive:
|
||||
break
|
||||
|
||||
# Wait for next request
|
||||
if not self.recv_buffer:
|
||||
await self._receive_more()
|
||||
|
||||
async def http1_request_header(self):
|
||||
"""
|
||||
Receive and parse request header into self.request.
|
||||
"""
|
||||
HEADER_MAX_SIZE = min(8192, self.request_max_size)
|
||||
# Receive until full header is in buffer
|
||||
buf = self.recv_buffer
|
||||
pos = 0
|
||||
|
||||
while True:
|
||||
pos = buf.find(b"\r\n\r\n", pos)
|
||||
if pos != -1:
|
||||
break
|
||||
|
||||
pos = max(0, len(buf) - 3)
|
||||
if pos >= HEADER_MAX_SIZE:
|
||||
break
|
||||
|
||||
await self._receive_more()
|
||||
|
||||
if pos >= HEADER_MAX_SIZE:
|
||||
raise PayloadTooLarge("Request header exceeds the size limit")
|
||||
|
||||
# Parse header content
|
||||
try:
|
||||
head = buf[:pos]
|
||||
raw_headers = head.decode(errors="surrogateescape")
|
||||
reqline, *split_headers = raw_headers.split("\r\n")
|
||||
method, self.url, protocol = reqline.split(" ")
|
||||
|
||||
if protocol == "HTTP/1.1":
|
||||
self.keep_alive = True
|
||||
elif protocol == "HTTP/1.0":
|
||||
self.keep_alive = False
|
||||
else:
|
||||
raise Exception # Raise a Bad Request on try-except
|
||||
|
||||
self.head_only = method.upper() == "HEAD"
|
||||
request_body = False
|
||||
headers = []
|
||||
|
||||
for name, value in (h.split(":", 1) for h in split_headers):
|
||||
name, value = h = name.lower(), value.lstrip()
|
||||
|
||||
if name in ("content-length", "transfer-encoding"):
|
||||
request_body = True
|
||||
elif name == "connection":
|
||||
self.keep_alive = value.lower() == "keep-alive"
|
||||
|
||||
headers.append(h)
|
||||
except Exception:
|
||||
raise InvalidUsage("Bad Request")
|
||||
|
||||
headers_instance = Header(headers)
|
||||
self.upgrade_websocket = headers_instance.get("upgrade") == "websocket"
|
||||
|
||||
# Prepare a Request object
|
||||
request = self.protocol.request_class(
|
||||
url_bytes=self.url.encode(),
|
||||
headers=headers_instance,
|
||||
head=bytes(head),
|
||||
version=protocol[5:],
|
||||
method=method,
|
||||
transport=self.protocol.transport,
|
||||
app=self.protocol.app,
|
||||
)
|
||||
|
||||
# Prepare for request body
|
||||
self.request_bytes_left = self.request_bytes = 0
|
||||
if request_body:
|
||||
headers = request.headers
|
||||
expect = headers.get("expect")
|
||||
|
||||
if expect is not None:
|
||||
if expect.lower() == "100-continue":
|
||||
self.expecting_continue = True
|
||||
else:
|
||||
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
||||
|
||||
if headers.get("transfer-encoding") == "chunked":
|
||||
self.request_body = "chunked"
|
||||
pos -= 2 # One CRLF stays in buffer
|
||||
else:
|
||||
self.request_body = True
|
||||
self.request_bytes_left = self.request_bytes = int(
|
||||
headers["content-length"]
|
||||
)
|
||||
|
||||
# Remove header and its trailing CRLF
|
||||
del buf[: pos + 4]
|
||||
self.stage = Stage.HANDLER
|
||||
self.request, request.stream = request, self
|
||||
self.protocol.state["requests_count"] += 1
|
||||
|
||||
async def http1_response_header(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
res = self.response
|
||||
|
||||
# Compatibility with simple response body
|
||||
if not data and getattr(res, "body", None):
|
||||
data, end_stream = res.body, True # type: ignore
|
||||
|
||||
size = len(data)
|
||||
headers = res.headers
|
||||
status = res.status
|
||||
|
||||
if not isinstance(status, int) or status < 200:
|
||||
raise RuntimeError(f"Invalid response status {status!r}")
|
||||
|
||||
if not has_message_body(status):
|
||||
# Header-only response status
|
||||
self.response_func = None
|
||||
if (
|
||||
data
|
||||
or not end_stream
|
||||
or "content-length" in headers
|
||||
or "transfer-encoding" in headers
|
||||
):
|
||||
data, size, end_stream = b"", 0, True
|
||||
headers.pop("content-length", None)
|
||||
headers.pop("transfer-encoding", None)
|
||||
logger.warning(
|
||||
f"Message body set in response on {self.request.path}. "
|
||||
f"A {status} response may only have headers, no body."
|
||||
)
|
||||
elif self.head_only and "content-length" in headers:
|
||||
self.response_func = None
|
||||
elif end_stream:
|
||||
# Non-streaming response (all in one block)
|
||||
headers["content-length"] = size
|
||||
self.response_func = None
|
||||
elif "content-length" in headers:
|
||||
# Streaming response with size known in advance
|
||||
self.response_bytes_left = int(headers["content-length"]) - size
|
||||
self.response_func = self.http1_response_normal
|
||||
else:
|
||||
# Length not known, use chunked encoding
|
||||
headers["transfer-encoding"] = "chunked"
|
||||
data = b"%x\r\n%b\r\n" % (size, data) if size else b""
|
||||
self.response_func = self.http1_response_chunked
|
||||
|
||||
if self.head_only:
|
||||
# Head request: don't send body
|
||||
data = b""
|
||||
self.response_func = self.head_response_ignored
|
||||
|
||||
headers["connection"] = "keep-alive" if self.keep_alive else "close"
|
||||
ret = format_http1_response(status, res.processed_headers)
|
||||
if data:
|
||||
ret += data
|
||||
|
||||
# Send a 100-continue if expected and not Expectation Failed
|
||||
if self.expecting_continue:
|
||||
self.expecting_continue = False
|
||||
if status != 417:
|
||||
ret = HTTP_CONTINUE + ret
|
||||
|
||||
# Send response
|
||||
if self.protocol.access_log:
|
||||
self.log_response()
|
||||
|
||||
await self._send(ret)
|
||||
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
|
||||
|
||||
def head_response_ignored(self, data: bytes, end_stream: bool) -> None:
|
||||
"""
|
||||
HEAD response: body data silently ignored.
|
||||
"""
|
||||
if end_stream:
|
||||
self.response_func = None
|
||||
self.stage = Stage.IDLE
|
||||
|
||||
async def http1_response_chunked(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
"""
|
||||
Format a part of response body in chunked encoding.
|
||||
"""
|
||||
# Chunked encoding
|
||||
size = len(data)
|
||||
if end_stream:
|
||||
await self._send(
|
||||
b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
|
||||
if size
|
||||
else b"0\r\n\r\n"
|
||||
)
|
||||
self.response_func = None
|
||||
self.stage = Stage.IDLE
|
||||
elif size:
|
||||
await self._send(b"%x\r\n%b\r\n" % (size, data))
|
||||
|
||||
async def http1_response_normal(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
"""
|
||||
Format / keep track of non-chunked response.
|
||||
"""
|
||||
bytes_left = self.response_bytes_left - len(data)
|
||||
if bytes_left <= 0:
|
||||
if bytes_left < 0:
|
||||
raise ServerError("Response was bigger than content-length")
|
||||
|
||||
await self._send(data)
|
||||
self.response_func = None
|
||||
self.stage = Stage.IDLE
|
||||
else:
|
||||
if end_stream:
|
||||
raise ServerError("Response was smaller than content-length")
|
||||
|
||||
await self._send(data)
|
||||
self.response_bytes_left = bytes_left
|
||||
|
||||
async def error_response(self, exception: Exception) -> None:
|
||||
"""
|
||||
Handle response when exception encountered
|
||||
"""
|
||||
# Disconnect after an error if in any other state than handler
|
||||
if self.stage is not Stage.HANDLER:
|
||||
self.keep_alive = False
|
||||
|
||||
# Request failure? Respond but then disconnect
|
||||
if self.stage is Stage.REQUEST:
|
||||
self.stage = Stage.HANDLER
|
||||
|
||||
# From request and handler states we can respond, otherwise be silent
|
||||
if self.stage is Stage.HANDLER:
|
||||
app = self.protocol.app
|
||||
|
||||
if self.request is None:
|
||||
self.create_empty_request()
|
||||
|
||||
await app.handle_exception(self.request, exception)
|
||||
|
||||
def create_empty_request(self) -> None:
|
||||
"""
|
||||
Current error handling code needs a request object that won't exist
|
||||
if an error occurred during before a request was received. Create a
|
||||
bogus response for error handling use.
|
||||
"""
|
||||
|
||||
# FIXME: Avoid this by refactoring error handling and response code
|
||||
self.request = self.protocol.request_class(
|
||||
url_bytes=self.url.encode() if self.url else b"*",
|
||||
headers=Header({}),
|
||||
version="1.1",
|
||||
method="NONE",
|
||||
transport=self.protocol.transport,
|
||||
app=self.protocol.app,
|
||||
)
|
||||
self.request.stream = self
|
||||
|
||||
def log_response(self) -> None:
|
||||
"""
|
||||
Helper method provided to enable the logging of responses in case if
|
||||
the :attr:`HttpProtocol.access_log` is enabled.
|
||||
"""
|
||||
req, res = self.request, self.response
|
||||
extra = {
|
||||
"status": getattr(res, "status", 0),
|
||||
"byte": getattr(self, "response_bytes_left", -1),
|
||||
"host": "UNKNOWN",
|
||||
"request": "nil",
|
||||
}
|
||||
if req is not None:
|
||||
if req.ip:
|
||||
extra["host"] = f"{req.ip}:{req.port}"
|
||||
extra["request"] = f"{req.method} {req.url}"
|
||||
access_logger.info("", extra=extra)
|
||||
|
||||
# Request methods
|
||||
|
||||
async def __aiter__(self):
|
||||
"""
|
||||
Async iterate over request body.
|
||||
"""
|
||||
while self.request_body:
|
||||
data = await self.read()
|
||||
|
||||
if data:
|
||||
yield data
|
||||
|
||||
async def read(self) -> Optional[bytes]:
|
||||
"""
|
||||
Read some bytes of request body.
|
||||
"""
|
||||
|
||||
# Send a 100-continue if needed
|
||||
if self.expecting_continue:
|
||||
self.expecting_continue = False
|
||||
await self._send(HTTP_CONTINUE)
|
||||
|
||||
# Receive request body chunk
|
||||
buf = self.recv_buffer
|
||||
if self.request_bytes_left == 0 and self.request_body == "chunked":
|
||||
# Process a chunk header: \r\n<size>[;<chunk extensions>]\r\n
|
||||
while True:
|
||||
pos = buf.find(b"\r\n", 3)
|
||||
|
||||
if pos != -1:
|
||||
break
|
||||
|
||||
if len(buf) > 64:
|
||||
self.keep_alive = False
|
||||
raise InvalidUsage("Bad chunked encoding")
|
||||
|
||||
await self._receive_more()
|
||||
|
||||
try:
|
||||
size = int(buf[2:pos].split(b";", 1)[0].decode(), 16)
|
||||
except Exception:
|
||||
self.keep_alive = False
|
||||
raise InvalidUsage("Bad chunked encoding")
|
||||
|
||||
del buf[: pos + 2]
|
||||
|
||||
if size <= 0:
|
||||
self.request_body = None
|
||||
|
||||
if size < 0:
|
||||
self.keep_alive = False
|
||||
raise InvalidUsage("Bad chunked encoding")
|
||||
|
||||
return None
|
||||
|
||||
self.request_bytes_left = size
|
||||
self.request_bytes += size
|
||||
|
||||
# Request size limit
|
||||
if self.request_bytes > self.request_max_size:
|
||||
self.keep_alive = False
|
||||
raise PayloadTooLarge("Request body exceeds the size limit")
|
||||
|
||||
# End of request body?
|
||||
if not self.request_bytes_left:
|
||||
self.request_body = None
|
||||
return None
|
||||
|
||||
# At this point we are good to read/return up to request_bytes_left
|
||||
if not buf:
|
||||
await self._receive_more()
|
||||
|
||||
data = bytes(buf[: self.request_bytes_left])
|
||||
size = len(data)
|
||||
|
||||
del buf[:size]
|
||||
|
||||
self.request_bytes_left -= size
|
||||
|
||||
return data
|
||||
|
||||
# Response methods
|
||||
|
||||
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse:
|
||||
"""
|
||||
Initiate new streaming response.
|
||||
|
||||
Nothing is sent until the first send() call on the returned object, and
|
||||
calling this function multiple times will just alter the response to be
|
||||
given.
|
||||
"""
|
||||
if self.stage is not Stage.HANDLER:
|
||||
self.stage = Stage.FAILED
|
||||
raise RuntimeError("Response already started")
|
||||
|
||||
self.response, response.stream = response, self
|
||||
return response
|
||||
|
||||
@property
|
||||
def send(self):
|
||||
return self.response_func
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user