Compare commits

..

1 Commits

Author SHA1 Message Date
Adam Hopkins
a61c5ff55b
Get better output on test failures 2022-02-27 13:29:16 +02:00
543 changed files with 7797 additions and 69516 deletions

2
.black.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.black]
line-length = 79

View File

@ -4,12 +4,11 @@ source = sanic
omit =
site-packages
sanic/__main__.py
sanic/server/legacy.py
sanic/compat.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
sanic/cli
sanic/pages
[html]
directory = coverage
@ -21,5 +20,12 @@ exclude_lines =
noqa
NOQA
pragma: no cover
TYPE_CHECKING
omit =
site-packages
sanic/__main__.py
sanic/compat.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
sanic/cli
skip_empty = True

View File

@ -1,78 +0,0 @@
name: 🐞 Bug report
description: Create a report to help us improve
labels: ["bug", "triage"]
body:
- type: checkboxes
id: existing
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks using markdown code-block syntax to make it easier to read.
validations:
required: true
- type: textarea
id: code
attributes:
label: Code snippet
description: |
Relevant source code, make sure to remove what is not necessary. Please try and format your code so that it is easier to read. For example:
```python
from sanic import Sanic
app = Sanic("Example")
```
validations:
required: false
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: dropdown
id: running
attributes:
label: How do you run Sanic?
options:
- Sanic CLI
- As a module
- As a script (`app.run` or `Sanic.serve`)
- ASGI
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
description: What OS?
options:
- Linux
- MacOS
- Windows
- Other (tell us in the description)
validations:
required: true
- type: input
id: version
attributes:
label: Sanic Version
description: Check startup logs or try `sanic --version`
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
**Code snippet**
Relevant source code, make sure to remove what is not necessary.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 0.8.3]
**Additional context**
Add any other context about the problem here.

View File

@ -1,8 +1,5 @@
blank_issues_enabled: false
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.
- name: Discussion and Support
url: https://discord.gg/FARQzAEMAA
about: For live discussion and support, checkout the Sanic Discord server.

View File

@ -1,34 +0,0 @@
name: 🌟 Feature request
description: Suggest an enhancement for Sanic
labels: ["feature request"]
body:
- type: checkboxes
id: existing
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the enhancement you are proposing.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: description
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false
- type: textarea
id: code
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

View File

@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest an idea for Sanic
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or sample code about the feature request here.

View File

@ -1,33 +0,0 @@
name: 💡 Request for Comments
description: Open an RFC for discussion
labels: ["RFC"]
body:
- type: input
id: compare
attributes:
label: Link to code
description: If available, share a [comparison](https://github.com/sanic-org/sanic/compare) from a POC branch to main
placeholder: https://github.com/sanic-org/sanic/compare/main...some-new-branch
validations:
required: false
- type: textarea
id: proposal
attributes:
label: Proposal
description: A thorough discussion of the proposal discussing the problem it solves, potential code, use cases, and impacts
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context that is relevant
validations:
required: false
- type: checkboxes
id: breaking
attributes:
label: Is this a breaking change?
options:
- label: "Yes"
required: false

View File

@ -4,12 +4,10 @@ on:
push:
branches:
- main
- current-release
- "*LTS"
pull_request:
branches:
- main
- current-release
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
schedule:

View File

@ -3,32 +3,36 @@ on:
push:
branches:
- main
- current-release
- "*LTS"
tags:
- "!*" # Do not execute on tags
pull_request:
branches:
- main
- current-release
- "*LTS"
jobs:
coverage:
name: Check coverage
runs-on: ubuntu-latest
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.9]
os: [ubuntu-latest]
fail-fast: false
steps:
- name: Run coverage
uses: sanic-org/simple-tox-action@v1
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
with:
python-version: "3.11"
tox-env: coverage
ignore-errors: true
- name: Run Codecov
uses: codecov/codecov-action@v3
python-version: ${{ matrix.python-version }}
- name: Install dependencies 🔨
run: |
python -m pip install --upgrade pip
pip install tox
- name: Run coverage
run: tox -e coverage
continue-on-error: true
- uses: codecov/codecov-action@v2
with:
files: ./coverage.xml
fail_ci_if_error: false

39
.github/workflows/on-demand.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: On Demand Task
on:
workflow_dispatch:
inputs:
python-version:
description: 'Version of Python to use for running Test'
required: false
default: "3.8"
tox-env:
description: 'Test Environment to Run'
required: true
default: ''
os:
description: 'Operating System to Run Test on'
required: false
default: ubuntu-latest
jobs:
onDemand:
name: tox-${{ matrix.config.tox-env }}-on-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["${{ github.event.inputs.os}}"]
config:
- { tox-env: "${{ github.event.inputs.tox-env }}", py-version: "${{ github.event.inputs.python-version }}"}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.py-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "yes"

36
.github/workflows/pr-bandit.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Security Analysis
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
bandit:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: security}
- { python-version: 3.8, tox-env: security}
- { python-version: 3.9, tox-env: security}
- { python-version: "3.10", tox-env: security}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

32
.github/workflows/pr-docs.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Document Linter
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation
runs-on: ubuntu-latest
strategy:
matrix:
config:
- {python-version: "3.8", tox-env: "docs"}
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run Document Linter
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

33
.github/workflows/pr-linter.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Linter Checks
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
linter:
if: github.event.pull_request.draft == false
name: lint
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: lint}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

41
.github/workflows/pr-python-pypy.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Python PyPy Tests
on:
workflow_dispatch:
inputs:
tox-env:
description: "Tox Env to run on the PyPy Infra"
required: false
default: "pypy37"
pypy-version:
description: "Version of PyPy to use"
required: false
default: "pypy-3.7"
jobs:
testPyPy:
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: "${{ github.event.inputs.pypy-version }}",
tox-env: "${{ github.event.inputs.tox-env }}",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "true"
command-timeout: "600000"

46
.github/workflows/pr-python310.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Python 3.10 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy310:
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: "3.10",
tox-env: py310,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: "3.10",
tox-env: py310-no-ext,
ignore-error-flake: "true",
command-timeout: "600000",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
command-timeout: "${{ matrix.config.command-timeout }}"
test-failure-retry: "3"

35
.github/workflows/pr-python37.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Python 3.7 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy37:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: py37 }
- { python-version: 3.7, tox-env: py37-no-ext }
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
test-failure-retry: "3"

35
.github/workflows/pr-python38.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Python 3.8 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy38:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: py38 }
- { python-version: 3.8, tox-env: py38-no-ext }
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
test-failure-retry: "3"

47
.github/workflows/pr-python39.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Python 3.9 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy39:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: 3.9,
tox-env: py39,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: 3.9,
tox-env: py39-no-ext,
ignore-error-flake: "true",
command-timeout: "600000",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
command-timeout: "${{ matrix.config.command-timeout }}"
test-failure-retry: "3"

36
.github/workflows/pr-type-check.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Typing Checks
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
typeChecking:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
# - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking}
- { python-version: "3.10", tox-env: type-checking}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

38
.github/workflows/pr-windows.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Run Unit Tests on Windows
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testsOnWindows:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
config:
- { python-version: 3.7, tox-env: py37-no-ext }
- { python-version: 3.8, tox-env: py38-no-ext }
- { python-version: 3.9, tox-env: py39-no-ext }
- { python-version: "3.10", tox-env: py310-no-ext }
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run Unit Tests
uses: ahopkins/custom-actions@pip-extra-args
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "true"
command-timeout: "600000"
pip-extra-args: "--user"

48
.github/workflows/publish-images.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: Publish Docker Images
on:
workflow_run:
workflows:
- 'Publish Artifacts'
types:
- completed
jobs:
publishDockerImages:
name: Docker Image Build [${{ matrix.python-version }}]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Build Latest Base images for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic-build
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-image-tag: "${{ matrix.python-version }}"
docker-file-suffix: "base"
docker-build-args: "PYTHON_VERSION=${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'
- name: Publish Sanic Docker Image for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-build-args: "BASE_IMAGE_TAG=${{ matrix.python-version }}"
docker-image-prefix: "${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'

28
.github/workflows/publish-package.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Publish Artifacts
on:
release:
types: [created]
jobs:
publishPythonPackage:
name: Publishing Sanic Release Artifacts
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.8"]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Publish Python Package
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.python-version }}
package-infra-name: "twine"
pypi-user: __token__
pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }}
action: "package-publish"
pypi-verify-metadata: "true"

View File

@ -1,174 +0,0 @@
name: Publish release
on:
release:
types: [created]
env:
IS_TEST: false
DOCKER_ORG_NAME: sanicframework
DOCKER_IMAGE_NAME: sanic
DOCKER_BASE_IMAGE_NAME: sanic-build
DOCKER_IMAGE_DOCKERFILE: ./docker/Dockerfile
DOCKER_BASE_IMAGE_DOCKERFILE: ./docker/Dockerfile-base
jobs:
generate_info:
name: Generate info
runs-on: ubuntu-latest
outputs:
docker-tags: ${{ steps.generate_docker_info.outputs.tags }}
pypi-version: ${{ steps.parse_version_tag.outputs.pypi-version }}
steps:
- name: Parse version tag
id: parse_version_tag
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: |
tag_name="${{ env.TAG_NAME }}"
if [[ ! "${tag_name}" =~ ^v([0-9]{2})\.([0-9]{1,2})\.([0-9]+)$ ]]; then
echo "::error::Tag name must be in the format vYY.MM.MICRO"
exit 1
fi
year_output="year=${BASH_REMATCH[1]}"
month_output="month=${BASH_REMATCH[2]}"
pypi_output="pypi-version=${tag_name#v}"
echo "${year_output}"
echo "${month_output}"
echo "${pypi_output}"
echo "${year_output}" >> $GITHUB_OUTPUT
echo "${month_output}" >> $GITHUB_OUTPUT
echo "${pypi_output}" >> $GITHUB_OUTPUT
- name: Get latest release
id: get_latest_release
run: |
latest_tag=$(
curl -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/releases/latest \
| jq -r '.tag_name'
)
echo "latest_tag=$latest_tag" >> $GITHUB_OUTPUT
- name: Generate Docker info
id: generate_docker_info
run: |
tag_year="${{ steps.parse_version_tag.outputs.year }}"
tag_month="${{ steps.parse_version_tag.outputs.month }}"
latest_tag="${{ steps.get_latest_release.outputs.latest_tag }}"
tag="${{ github.event.release.tag_name }}"
tags="${tag_year}.${tag_month}"
if [[ "${tag_month}" == "12" ]]; then
tags+=",LTS"
echo "::notice::Tag ${tag} is LTS version"
else
echo "::notice::Tag ${tag} is not LTS version"
fi
if [[ "${latest_tag}" == "${{ github.event.release.tag_name }}" ]]; then
tags+=",latest"
echo "::notice::Tag ${tag} is marked as latest"
else
echo "::notice::Tag ${tag} is not marked as latest"
fi
tags_output="tags=${tags}"
echo "${tags_output}"
echo "${tags_output}" >> $GITHUB_OUTPUT
publish_package:
name: Build and publish package
runs-on: ubuntu-latest
needs: generate_info
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: pip install build twine
- name: Update package version
run: |
echo "__version__ = \"${{ needs.generate_info.outputs.pypi-version }}\"" > sanic/__version__.py
- name: Build a binary wheel and a source tarball
run: python -m build --sdist --wheel --outdir dist/ .
- name: Publish to PyPi 🚀
run: twine upload --non-interactive --disable-progress-bar dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ env.IS_TEST == 'true' && secrets.SANIC_TEST_PYPI_API_TOKEN || secrets.SANIC_PYPI_API_TOKEN }}
TWINE_REPOSITORY: ${{ env.IS_TEST == 'true' && 'testpypi' || 'pypi' }}
publish_docker:
name: Publish Docker / Python ${{ matrix.python-version }}
needs: [generate_info, publish_package]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.10", "3.11"]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_ACCESS_USER }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Build and push base image
uses: docker/build-push-action@v4
with:
push: ${{ env.IS_TEST == 'false' }}
file: ${{ env.DOCKER_BASE_IMAGE_DOCKERFILE }}
tags: ${{ env.DOCKER_ORG_NAME }}/${{ env.DOCKER_BASE_IMAGE_NAME }}:${{ matrix.python-version }}
build-args: |
PYTHON_VERSION=${{ matrix.python-version }}
- name: Parse tags for this Python version
id: parse_tags
run: |
IFS=',' read -ra tags <<< "${{ needs.generate_info.outputs.docker-tags }}"
tag_args=""
for tag in "${tags[@]}"; do
tag_args+=",${{ env.DOCKER_ORG_NAME }}/${{ env.DOCKER_IMAGE_NAME }}:${tag}-py${{ matrix.python-version }}"
done
tag_args_output="tag_args=${tag_args:1}"
echo "${tag_args_output}"
echo "${tag_args_output}" >> $GITHUB_OUTPUT
- name: Build and push Sanic image
uses: docker/build-push-action@v4
with:
push: ${{ env.IS_TEST == 'false' }}
file: ${{ env.DOCKER_IMAGE_DOCKERFILE }}
tags: ${{ steps.parse_tags.outputs.tag_args }}
build-args: |
BASE_IMAGE_ORG=${{ env.DOCKER_ORG_NAME }}
BASE_IMAGE_NAME=${{ env.DOCKER_BASE_IMAGE_NAME }}
BASE_IMAGE_TAG=${{ matrix.python-version }}
SANIC_PYPI_VERSION=${{ needs.generate_info.outputs.pypi-version }}

View File

@ -1,56 +0,0 @@
name: Tests
on:
push:
branches:
- main
- current-release
- "*LTS"
tags:
- "!*"
pull_request:
branches:
- main
- current-release
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
run_tests:
name: "${{ matrix.config.platform == 'windows-latest' && 'Windows' || 'Linux' }} / Python ${{ matrix.config.python-version }} / tox -e ${{ matrix.config.tox-env }}"
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.config.platform || 'ubuntu-latest' }}
strategy:
fail-fast: true
matrix:
config:
- { python-version: "3.8", tox-env: security }
- { python-version: "3.9", tox-env: security }
- { python-version: "3.10", tox-env: security }
- { python-version: "3.11", tox-env: security }
- { python-version: "3.10", tox-env: lint }
# - { python-version: "3.10", tox-env: docs }
- { python-version: "3.8", tox-env: type-checking }
- { python-version: "3.9", tox-env: type-checking }
- { python-version: "3.10", tox-env: type-checking }
- { python-version: "3.11", tox-env: type-checking }
- { python-version: "3.8", tox-env: py38, max-attempts: 3 }
- { python-version: "3.8", tox-env: py38-no-ext, max-attempts: 3 }
- { python-version: "3.9", tox-env: py39, max-attempts: 3 }
- { python-version: "3.9", tox-env: py39-no-ext, max-attempts: 3 }
- { python-version: "3.10", tox-env: py310, max-attempts: 3 }
- { python-version: "3.10", tox-env: py310-no-ext, max-attempts: 3 }
- { python-version: "3.11", tox-env: py311, max-attempts: 3 }
- { python-version: "3.11", tox-env: py311-no-ext, max-attempts: 3 }
- { python-version: "3.8", tox-env: py38-no-ext, platform: windows-latest, ignore-errors: true }
- { python-version: "3.9", tox-env: py39-no-ext, platform: windows-latest, ignore-errors: true }
- { python-version: "3.10", tox-env: py310-no-ext, platform: windows-latest, ignore-errors: true }
- { python-version: "3.11", tox-env: py310-no-ext, platform: windows-latest, ignore-errors: true }
steps:
- name: Run tests
uses: sanic-org/simple-tox-action@v1
with:
python-version: ${{ matrix.config.python-version }}
tox-env: ${{ matrix.config.tox-env }}
max-attempts: ${{ matrix.config.max-attempts || 1 }}
ignore-errors: ${{ matrix.config.ignore-errors || false }}

2
.gitignore vendored
View File

@ -21,6 +21,4 @@ dist/*
pip-wheel-metadata/
.pytest_cache/*
.venv/*
venv/*
.vscode/*
guide/node_modules/

View File

@ -357,6 +357,11 @@ Version 20.12.0
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
Add disable app registry
Version 20.12.0
---------------
**Features**
*
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
Static route more verbose if file not found

View File

@ -1,74 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at adam@sanicframework.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at adam@sanicframework.org. All
reported by contacting the project team at sanic-maintainers@googlegroups.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.

View File

@ -71,9 +71,9 @@ To execute only unittests, run ``tox`` with environment like so:
.. code-block:: bash
tox -e py37 -v -- tests/test_config.py
tox -e py36 -v -- tests/test_config.py
# or
tox -e py310 -v -- tests/test_config.py
tox -e py37 -v -- tests/test_config.py
Run lint checks
---------------

View File

@ -66,15 +66,15 @@ ifdef include_tests
isort -rc sanic tests
else
$(info Sorting Imports)
isort -rc sanic tests
isort -rc sanic tests --profile=black
endif
endif
black:
black sanic tests
black --config ./.black.toml sanic tests
isort:
isort sanic tests
isort sanic tests --profile=black
pretty: black isort
@ -93,9 +93,6 @@ docs-serve:
changelog:
python scripts/changelog.py
guide-serve:
cd guide && sanic server:app -r -R ./content -R ./style
release:
ifdef version
python scripts/release.py --release-version ${version} --generate-changelog

View File

@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1
* - Build
- | |Tests|
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
* - Docs
- | |UserGuide| |Documentation|
* - Package
@ -19,7 +19,7 @@ Sanic | Build fast. Run fast.
* - Support
- | |Forums| |Discord| |Awesome|
* - Stats
- | |Monthly Downloads| |Weekly Downloads| |Conda downloads|
- | |Downloads| |WkDownloads| |Conda downloads|
.. |UserGuide| image:: https://img.shields.io/badge/user%20guide-sanic-ff0068
:target: https://sanicframework.org/
@ -27,8 +27,14 @@ Sanic | Build fast. Run fast.
:target: https://community.sanicframework.org/
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
:target: https://discord.gg/FARQzAEMAA
.. |Tests| image:: https://github.com/sanic-org/sanic/actions/workflows/tests.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/tests.yml
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml
.. |Py37Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml
.. |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
@ -46,23 +52,19 @@ Sanic | Build fast. Run fast.
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
:alt: Awesome Sanic List
:target: https://github.com/mekicha/awesome-sanic
.. |Monthly Downloads| image:: https://img.shields.io/pypi/dm/sanic.svg
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
:alt: Downloads
:target: https://pepy.tech/project/sanic
.. |Weekly Downloads| image:: https://img.shields.io/pypi/dw/sanic.svg
.. |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
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
:alt: Linode
:target: https://www.linode.com
:width: 200px
.. end-badges
Sanic is a **Python 3.8+** 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 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://sanicframework.org/en/guide/deployment/running.html#asgi>`_.
@ -100,6 +102,9 @@ Installation
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.
Hello World Example
-------------------
@ -109,7 +114,7 @@ Hello World Example
from sanic import Sanic
from sanic.response import json
app = Sanic("my-hello-world-app")
app = Sanic("My Hello, world app")
@app.route('/')
async def test(request):
@ -139,17 +144,17 @@ And, we can verify it is working: ``curl localhost:8000 -i``
**Now, let's go build something fast!**
Minimum Python version is 3.8. If you need Python 3.7 support, please use v22.12LTS.
Minimum Python version is 3.7. If you need Python 3.6 support, please use v20.12LTS.
Documentation
-------------
`User Guide <https://sanic.dev>`__ and `API Documentation <http://sanic.readthedocs.io/>`__.
`User Guide <https://sanicframework.org>`__ and `API Documentation <http://sanic.readthedocs.io/>`__.
Changelog
---------
`Release Changelogs <https://sanic.readthedocs.io/en/stable/sanic/changelog.html>`__.
`Release Changelogs <https://github.com/sanic-org/sanic/blob/master/CHANGELOG.rst>`__.
Questions and Discussion
@ -161,3 +166,8 @@ 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>`_.
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
:alt: Linode
:target: https://www.linode.com
:width: 200px

View File

@ -4,42 +4,31 @@
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: |
| Version | LTS | Supported |
| ------- | ------------- | ----------------------- |
| 22.12 | until 2024-12 | :white_check_mark: |
| 22.9 | | :x: |
| 22.6 | | :x: |
| 22.3 | | :x: |
| 21.12 | until 2023-12 | :ballot_box_with_check: |
| 21.9 | | :x: |
| 21.6 | | :x: |
| 21.3 | | :x: |
| 20.12 | | :x: |
| 20.9 | | :x: |
| 20.6 | | :x: |
| 20.3 | | :x: |
| 19.12 | | :x: |
| 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: |
:ballot_box_with_check: = security/bug fixes
:white_check_mark: = full support
: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.
Alternatively, you can send a private message to Adam Hopkins on Discord. Find him on the [Sanic discord server](https://discord.gg/FARQzAEMAA).
This will help to not publicize the issue until the team can address it and resolve it.

View File

@ -4,7 +4,6 @@ coverage:
default:
target: auto
threshold: 0.75
informational: true
project:
default:
target: auto
@ -15,10 +14,10 @@ codecov:
ignore:
- "sanic/__main__.py"
- "sanic/compat.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- "sanic/cli/"
- "sanic/pages/"
- "sanic/cli"
- ".github/"
- "changelogs/"
- "docker/"

View File

@ -1,13 +1,9 @@
ARG BASE_IMAGE_ORG
ARG BASE_IMAGE_NAME
ARG BASE_IMAGE_TAG
FROM ${BASE_IMAGE_ORG}/${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}
FROM sanicframework/sanic-build:${BASE_IMAGE_TAG}
RUN apk update
RUN update-ca-certificates
ARG SANIC_PYPI_VERSION
RUN pip install -U pip && pip install sanic==${SANIC_PYPI_VERSION}
RUN pip install sanic
RUN apk del build-base

View File

@ -2,12 +2,3 @@
.wy-nav-top {
background: #444444;
}
#changelog section {
padding-left: 3rem;
}
#changelog section h2,
#changelog section h3 {
margin-left: -3rem;
}

View File

@ -24,11 +24,7 @@ import sanic
# -- General configuration ------------------------------------------------
extensions = [
"sphinx.ext.autodoc",
"m2r2",
"enum_tools.autoenum",
]
extensions = ["sphinx.ext.autodoc", "m2r2"]
templates_path = ["_templates"]

View File

@ -9,7 +9,7 @@ API
===
.. toctree::
:maxdepth: 3
:maxdepth: 2
👥 User Guide <https://sanicframework.org/guide/>
sanic/api_reference

View File

@ -15,19 +15,3 @@ sanic.config
.. automodule:: sanic.config
:members:
:show-inheritance:
sanic.application.constants
---------------------------
.. automodule:: sanic.application.constants
:exclude-members: StrEnum
:members:
:show-inheritance:
:inherited-members:
sanic.application.state
-----------------------
.. automodule:: sanic.application.state
:members:
:show-inheritance:

View File

@ -17,14 +17,6 @@ sanic.handlers
:show-inheritance:
sanic.headers
--------------
.. automodule:: sanic.headers
:members:
:show-inheritance:
sanic.request
-------------

View File

@ -16,3 +16,10 @@ sanic.server
:members:
:show-inheritance:
sanic.worker
------------
.. automodule:: sanic.worker
:members:
:show-inheritance:

View File

@ -1,16 +1,6 @@
📜 Changelog
============
| 🔶 Current release
| 🔷 In support release
|
.. mdinclude:: ./releases/23/23.6.md
.. mdinclude:: ./releases/23/23.3.md
.. mdinclude:: ./releases/22/22.12.md
.. mdinclude:: ./releases/22/22.9.md
.. mdinclude:: ./releases/22/22.6.md
.. mdinclude:: ./releases/22/22.3.md
.. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md
.. include:: ../../CHANGELOG.rst

View File

@ -1,12 +1,10 @@
## Version 21.12.1 🔷
_Current LTS version_
## Version 21.12.1
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
## Version 21.12.0 🔹
## Version 21.12.0
### Features
- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects

View File

@ -1,55 +0,0 @@
## Version 22.12.0 🔷
_Current version_
### Features
- [#2569](https://github.com/sanic-org/sanic/pull/2569) Add `JSONResponse` class with some convenient methods when updating a response object
- [#2598](https://github.com/sanic-org/sanic/pull/2598) Change `uvloop` requirement to `>=0.15.0`
- [#2609](https://github.com/sanic-org/sanic/pull/2609) Add compatibility with `websockets` v11.0
- [#2610](https://github.com/sanic-org/sanic/pull/2610) Kill server early on worker error
- Raise deadlock timeout to 30s
- [#2617](https://github.com/sanic-org/sanic/pull/2617) Scale number of running server workers
- [#2621](https://github.com/sanic-org/sanic/pull/2621) [#2634](https://github.com/sanic-org/sanic/pull/2634) Send `SIGKILL` on subsequent `ctrl+c` to force worker exit
- [#2622](https://github.com/sanic-org/sanic/pull/2622) Add API to restart all workers from the multiplexer
- [#2624](https://github.com/sanic-org/sanic/pull/2624) Default to `spawn` for all subprocesses unless specifically set:
```python
from sanic import Sanic
Sanic.start_method = "fork"
```
- [#2625](https://github.com/sanic-org/sanic/pull/2625) Filename normalisation of form-data/multipart file uploads
- [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector:
- Remote access to inspect running Sanic instances
- TLS support for encrypted calls to Inspector
- Authentication to Inspector with API key
- Ability to extend Inspector with custom commands
- [#2632](https://github.com/sanic-org/sanic/pull/2632) Control order of restart operations
- [#2633](https://github.com/sanic-org/sanic/pull/2633) Move reload interval to class variable
- [#2636](https://github.com/sanic-org/sanic/pull/2636) Add `priority` to `register_middleware` method
- [#2639](https://github.com/sanic-org/sanic/pull/2639) Add `unquote` to `add_route` method
- [#2640](https://github.com/sanic-org/sanic/pull/2640) ASGI websockets to receive `text` or `bytes`
### Bugfixes
- [#2607](https://github.com/sanic-org/sanic/pull/2607) Force socket shutdown before close to allow rebinding
- [#2590](https://github.com/sanic-org/sanic/pull/2590) Use actual `StrEnum` in Python 3.11+
- [#2615](https://github.com/sanic-org/sanic/pull/2615) Ensure middleware executes only once per request timeout
- [#2627](https://github.com/sanic-org/sanic/pull/2627) Crash ASGI application on lifespan failure
- [#2635](https://github.com/sanic-org/sanic/pull/2635) Resolve error with low-level server creation on Windows
### Deprecations and Removals
- [#2608](https://github.com/sanic-org/sanic/pull/2608) [#2630](https://github.com/sanic-org/sanic/pull/2630) Signal conditions and triggers saved on `signal.extra`
- [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector
- 🚨 *BREAKING CHANGE*: Moves the Inspector to a Sanic app from a simple TCP socket with a custom protocol
- *DEPRECATE*: The `--inspect*` commands have been deprecated in favor of `inspect ...` commands
- [#2628](https://github.com/sanic-org/sanic/pull/2628) Replace deprecated `distutils.strtobool`
### Developer infrastructure
- [#2612](https://github.com/sanic-org/sanic/pull/2612) Add CI testing for Python 3.11

View File

@ -1,52 +0,0 @@
## Version 22.3.0
### Features
- [#2347](https://github.com/sanic-org/sanic/pull/2347) API for multi-application server
- 🚨 *BREAKING CHANGE*: The old `sanic.worker.GunicornWorker` has been **removed**. To run Sanic with `gunicorn`, you should use it thru `uvicorn` [as described in their docs](https://www.uvicorn.org/#running-with-gunicorn).
- 🧁 *SIDE EFFECT*: Named background tasks are now supported, even in Python 3.7
- [#2357](https://github.com/sanic-org/sanic/pull/2357) Parse `Authorization` header as `Request.credentials`
- [#2361](https://github.com/sanic-org/sanic/pull/2361) Add config option to skip `Touchup` step in application startup
- [#2372](https://github.com/sanic-org/sanic/pull/2372) Updates to CLI help messaging
- [#2382](https://github.com/sanic-org/sanic/pull/2382) Downgrade warnings to backwater debug messages
- [#2396](https://github.com/sanic-org/sanic/pull/2396) Allow for `multidict` v0.6
- [#2401](https://github.com/sanic-org/sanic/pull/2401) Upgrade CLI catching for alternative application run types
- [#2402](https://github.com/sanic-org/sanic/pull/2402) Conditionally inject CLI arguments into factory
- [#2413](https://github.com/sanic-org/sanic/pull/2413) Add new start and stop event listeners to reloader process
- [#2414](https://github.com/sanic-org/sanic/pull/2414) Remove loop as required listener arg
- [#2415](https://github.com/sanic-org/sanic/pull/2415) Better exception for bad URL parsing
- [sanic-routing#47](https://github.com/sanic-org/sanic-routing/pull/47) Add a new extention parameter type: `<file:ext>`, `<file:ext=jpg>`, `<file:ext=jpg|png|gif|svg>`, `<file=int:ext>`, `<file=int:ext=jpg|png|gif|svg>`, `<file=float:ext=tar.gz>`
- 👶 *BETA FEATURE*: This feature will not work with `path` type matching, and is being released as a beta feature only.
- [sanic-routing#57](https://github.com/sanic-org/sanic-routing/pull/57) Change `register_pattern` to accept a `str` or `Pattern`
- [sanic-routing#58](https://github.com/sanic-org/sanic-routing/pull/58) Default matching on non-empty strings only, and new `strorempty` pattern type
- 🚨 *BREAKING CHANGE*: Previously a route with a dynamic string parameter (`/<foo>` or `/<foo:str>`) would match on any string, including empty strings. It will now **only** match a non-empty string. To retain the old behavior, you should use the new parameter type: `/<foo:strorempty>`.
### Bugfixes
- [#2373](https://github.com/sanic-org/sanic/pull/2373) Remove `error_logger` on websockets
- [#2381](https://github.com/sanic-org/sanic/pull/2381) Fix newly assigned `None` in task registry
- [sanic-routing#52](https://github.com/sanic-org/sanic-routing/pull/52) Add type casting to regex route matching
- [sanic-routing#60](https://github.com/sanic-org/sanic-routing/pull/60) Add requirements check on regex routes (this resolves, for example, multiple static directories with differing `host` values)
### Deprecations and Removals
- [#2362](https://github.com/sanic-org/sanic/pull/2362) 22.3 Deprecations and changes
1. `debug=True` and `--debug` do _NOT_ automatically run `auto_reload`
2. Default error render is with plain text (browsers still get HTML by default because `auto` looks at headers)
3. `config` is required for `ErrorHandler.finalize`
4. `ErrorHandler.lookup` requires two positional args
5. Unused websocket protocol args removed
- [#2344](https://github.com/sanic-org/sanic/pull/2344) Deprecate loading of lowercase environment variables
### Developer infrastructure
- [#2363](https://github.com/sanic-org/sanic/pull/2363) Revert code coverage back to Codecov
- [#2405](https://github.com/sanic-org/sanic/pull/2405) Upgrade tests for `sanic-routing` changes
- [sanic-testing#35](https://github.com/sanic-org/sanic-testing/pull/35) Allow for httpx v0.22
### Improved Documentation
- [#2350](https://github.com/sanic-org/sanic/pull/2350) Fix link in README for ASGI
- [#2398](https://github.com/sanic-org/sanic/pull/2398) Document middleware on_request and on_response
- [#2409](https://github.com/sanic-org/sanic/pull/2409) Add missing documentation for `Request.respond`
### Miscellaneous
- [#2376](https://github.com/sanic-org/sanic/pull/2376) Fix typing for `ListenerMixin.listener`
- [#2383](https://github.com/sanic-org/sanic/pull/2383) Clear deprecation warning in `asyncio.wait`
- [#2387](https://github.com/sanic-org/sanic/pull/2387) Cleanup `__slots__` implementations
- [#2390](https://github.com/sanic-org/sanic/pull/2390) Clear deprecation warning in `asyncio.get_event_loop`

View File

@ -1,54 +0,0 @@
## Version 22.6.2
### Bugfixes
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
## Version 22.6.1
### Bugfixes
- [#2477](https://github.com/sanic-org/sanic/pull/2477) Sanic static directory fails when folder name ends with ".."
## Version 22.6.0
### Features
- [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode
- 👶 *EARLY RELEASE FEATURE*: Serving Sanic over HTTP/3 is an early release feature. It does not yet fully cover the HTTP/3 spec, but instead aims for feature parity with Sanic's existing HTTP/1.1 server. Websockets, WebTransport, push responses are examples of some features not yet implemented.
- 📦 *EXTRA REQUIREMENT*: Not all HTTP clients are capable of interfacing with HTTP/3 servers. You may need to install a [HTTP/3 capable client](https://curl.se/docs/http3.html).
- 📦 *EXTRA REQUIREMENT*: In order to use TLS autogeneration, you must install either [mkcert](https://github.com/FiloSottile/mkcert) or [trustme](https://github.com/python-trio/trustme).
- [#2416](https://github.com/sanic-org/sanic/pull/2416) Add message to `task.cancel`
- [#2420](https://github.com/sanic-org/sanic/pull/2420) Add exception aliases for more consistent naming with standard HTTP response types (`BadRequest`, `MethodNotAllowed`, `RangeNotSatisfiable`)
- [#2432](https://github.com/sanic-org/sanic/pull/2432) Expose ASGI `scope` as a property on the `Request` object
- [#2438](https://github.com/sanic-org/sanic/pull/2438) Easier access to websocket class for annotation: `from sanic import Websocket`
- [#2439](https://github.com/sanic-org/sanic/pull/2439) New API for reading form values with options: `Request.get_form`
- [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom `loads` function
- [#2447](https://github.com/sanic-org/sanic/pull/2447), [#2486](https://github.com/sanic-org/sanic/pull/2486) Improved API to support setting cache control headers
- [#2453](https://github.com/sanic-org/sanic/pull/2453) Move verbosity filtering to logger
- [#2475](https://github.com/sanic-org/sanic/pull/2475) Expose getter for current request using `Request.get_current()`
### Bugfixes
- [#2448](https://github.com/sanic-org/sanic/pull/2448) Fix to allow running with `pythonw.exe` or places where there is no `sys.stdout`
- [#2451](https://github.com/sanic-org/sanic/pull/2451) Trigger `http.lifecycle.request` signal in ASGI mode
- [#2455](https://github.com/sanic-org/sanic/pull/2455) Resolve typing of stacked route definitions
- [#2463](https://github.com/sanic-org/sanic/pull/2463) Properly catch websocket CancelledError in websocket handler in Python 3.7
### Deprecations and Removals
- [#2487](https://github.com/sanic-org/sanic/pull/2487) v22.6 deprecations and changes
1. Optional application registry
1. Execution of custom handlers after some part of response was sent
1. Configuring fallback handlers on the `ErrorHandler`
1. Custom `LOGO` setting
1. `sanic.response.stream`
1. `AsyncioServer.init`
### Developer infrastructure
- [#2449](https://github.com/sanic-org/sanic/pull/2449) Clean up `black` and `isort` config
- [#2479](https://github.com/sanic-org/sanic/pull/2479) Fix some flappy tests
### Improved Documentation
- [#2461](https://github.com/sanic-org/sanic/pull/2461) Update example to match current application naming standards
- [#2466](https://github.com/sanic-org/sanic/pull/2466) Better type annotation for `Extend`
- [#2485](https://github.com/sanic-org/sanic/pull/2485) Improved help messages in CLI

View File

@ -1,74 +0,0 @@
## Version 22.9.1
### Features
- [#2585](https://github.com/sanic-org/sanic/pull/2585) Improved error message when no applications have been registered
### Bugfixes
- [#2578](https://github.com/sanic-org/sanic/pull/2578) Add certificate loader for in process certificate creation
- [#2591](https://github.com/sanic-org/sanic/pull/2591) Do not use sentinel identity for `spawn` compatibility
- [#2592](https://github.com/sanic-org/sanic/pull/2592) Fix properties in nested blueprint groups
- [#2595](https://github.com/sanic-org/sanic/pull/2595) Introduce sleep interval on new worker reloader
### Deprecations and Removals
### Developer infrastructure
- [#2588](https://github.com/sanic-org/sanic/pull/2588) Markdown templates on issue forms
### Improved Documentation
- [#2556](https://github.com/sanic-org/sanic/pull/2556) v22.9 documentation
- [#2582](https://github.com/sanic-org/sanic/pull/2582) Cleanup documentation on Windows support
## Version 22.9.0
### Features
- [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom loads function
- [#2490](https://github.com/sanic-org/sanic/pull/2490) Make `WebsocketImplProtocol` async iterable
- [#2499](https://github.com/sanic-org/sanic/pull/2499) Sanic Server WorkerManager refactor
- [#2506](https://github.com/sanic-org/sanic/pull/2506) Use `pathlib` for path resolution (for static file serving)
- [#2508](https://github.com/sanic-org/sanic/pull/2508) Use `path.parts` instead of `match` (for static file serving)
- [#2513](https://github.com/sanic-org/sanic/pull/2513) Better request cancel handling
- [#2516](https://github.com/sanic-org/sanic/pull/2516) Add request properties for HTTP method info:
- `request.is_safe`
- `request.is_idempotent`
- `request.is_cacheable`
- *See* [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) *for more information about when these apply*
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
- [#2526](https://github.com/sanic-org/sanic/pull/2526) Cache control support for static files for returning 304 when appropriate
- [#2533](https://github.com/sanic-org/sanic/pull/2533) Refactor `_static_request_handler`
- [#2540](https://github.com/sanic-org/sanic/pull/2540) Add signals before and after handler execution
- `http.handler.before`
- `http.handler.after`
- [#2542](https://github.com/sanic-org/sanic/pull/2542) Add *[redacted]* to CLI :)
- [#2546](https://github.com/sanic-org/sanic/pull/2546) Add deprecation warning filter
- [#2550](https://github.com/sanic-org/sanic/pull/2550) Middleware priority and performance enhancements
### Bugfixes
- [#2495](https://github.com/sanic-org/sanic/pull/2495) Prevent directory traversion with static files
- [#2515](https://github.com/sanic-org/sanic/pull/2515) Do not apply double slash to paths in certain static dirs in Blueprints
### Deprecations and Removals
- [#2525](https://github.com/sanic-org/sanic/pull/2525) Warn on duplicate route names, will be prevented outright in v23.3
- [#2537](https://github.com/sanic-org/sanic/pull/2537) Raise warning and deprecation notice on duplicate exceptions, will be prevented outright in v23.3
### Developer infrastructure
- [#2504](https://github.com/sanic-org/sanic/pull/2504) Cleanup test suite
- [#2505](https://github.com/sanic-org/sanic/pull/2505) Replace Unsupported Python Version Number from the Contributing Doc
- [#2530](https://github.com/sanic-org/sanic/pull/2530) Do not include tests folder in installed package resolver
### Improved Documentation
- [#2502](https://github.com/sanic-org/sanic/pull/2502) Fix a few typos
- [#2517](https://github.com/sanic-org/sanic/pull/2517) [#2536](https://github.com/sanic-org/sanic/pull/2536) Add some type hints

View File

@ -1,53 +0,0 @@
## Version 23.3.0
### Features
- [#2545](https://github.com/sanic-org/sanic/pull/2545) Standardize init of exceptions for more consistent control of HTTP responses using exceptions
- [#2606](https://github.com/sanic-org/sanic/pull/2606) Decode headers as UTF-8 also in ASGI
- [#2646](https://github.com/sanic-org/sanic/pull/2646) Separate ASGI request and lifespan callables
- [#2659](https://github.com/sanic-org/sanic/pull/2659) Use ``FALLBACK_ERROR_FORMAT`` for handlers that return ``empty()``
- [#2662](https://github.com/sanic-org/sanic/pull/2662) Add basic file browser (HTML page) and auto-index serving
- [#2667](https://github.com/sanic-org/sanic/pull/2667) Nicer traceback formatting (HTML page)
- [#2668](https://github.com/sanic-org/sanic/pull/2668) Smarter error page rendering format selection; more reliant upon header and "common sense" defaults
- [#2680](https://github.com/sanic-org/sanic/pull/2680) Check the status of socket before shutting down with ``SHUT_RDWR``
- [#2687](https://github.com/sanic-org/sanic/pull/2687) Refresh ``Request.accept`` functionality to be more performant and spec-compliant
- [#2696](https://github.com/sanic-org/sanic/pull/2696) Add header accessors as properties
```
Example-Field: Foo, Bar
Example-Field: Baz
```
```python
request.headers.example_field == "Foo, Bar,Baz"
```
- [#2700](https://github.com/sanic-org/sanic/pull/2700) Simpler CLI targets
```sh
$ sanic path.to.module:app # global app instance
$ sanic path.to.module:create_app # factory pattern
$ sanic ./path/to/directory/ # simple serve
```
- [#2701](https://github.com/sanic-org/sanic/pull/2701) API to define a number of workers in managed processes
- [#2704](https://github.com/sanic-org/sanic/pull/2704) Add convenience for dynamic changes to routing
- [#2706](https://github.com/sanic-org/sanic/pull/2706) Add convenience methods for cookie creation and deletion
```python
response = text("...")
response.add_cookie("test", "It worked!", domain=".yummy-yummy-cookie.com")
```
- [#2707](https://github.com/sanic-org/sanic/pull/2707) Simplified ``parse_content_header`` escaping to be RFC-compliant and remove outdated FF hack
- [#2710](https://github.com/sanic-org/sanic/pull/2710) Stricter charset handling and escaping of request URLs
- [#2711](https://github.com/sanic-org/sanic/pull/2711) Consume body on ``DELETE`` by default
- [#2719](https://github.com/sanic-org/sanic/pull/2719) Allow ``password`` to be passed to TLS context
- [#2720](https://github.com/sanic-org/sanic/pull/2720) Skip middleware on ``RequestCancelled``
- [#2721](https://github.com/sanic-org/sanic/pull/2721) Change access logging format to ``%s``
- [#2722](https://github.com/sanic-org/sanic/pull/2722) Add ``CertLoader`` as application option for directly controlling ``SSLContext`` objects
- [#2725](https://github.com/sanic-org/sanic/pull/2725) Worker sync state tolerance on race condition
### Bugfixes
- [#2651](https://github.com/sanic-org/sanic/pull/2651) ASGI websocket to pass thru bytes as is
- [#2697](https://github.com/sanic-org/sanic/pull/2697) Fix comparison between datetime aware and naive in ``file`` when using ``If-Modified-Since``
### Deprecations and Removals
- [#2666](https://github.com/sanic-org/sanic/pull/2666) Remove deprecated ``__blueprintname__`` property
### Improved Documentation
- [#2712](https://github.com/sanic-org/sanic/pull/2712) Improved example using ``'https'`` to create the redirect

View File

@ -1,33 +0,0 @@
## Version 23.6.0 🔶
### Features
- [#2670](https://github.com/sanic-org/sanic/pull/2670) Increase `KEEP_ALIVE_TIMEOUT` default to 120 seconds
- [#2716](https://github.com/sanic-org/sanic/pull/2716) Adding allow route overwrite option in blueprint
- [#2724](https://github.com/sanic-org/sanic/pull/2724) and [#2792](https://github.com/sanic-org/sanic/pull/2792) Add a new exception signal for ALL exceptions raised anywhere in application
- [#2727](https://github.com/sanic-org/sanic/pull/2727) Add name prefixing to BP groups
- [#2754](https://github.com/sanic-org/sanic/pull/2754) Update request type on middleware types
- [#2770](https://github.com/sanic-org/sanic/pull/2770) Better exception message on startup time application induced import error
- [#2776](https://github.com/sanic-org/sanic/pull/2776) Set multiprocessing start method early
- [#2785](https://github.com/sanic-org/sanic/pull/2785) Add custom typing to config and ctx objects
- [#2790](https://github.com/sanic-org/sanic/pull/2790) Add `request.client_ip`
### Bugfixes
- [#2728](https://github.com/sanic-org/sanic/pull/2728) Fix traversals for intended results
- [#2729](https://github.com/sanic-org/sanic/pull/2729) Handle case when headers argument of ResponseStream constructor is None
- [#2737](https://github.com/sanic-org/sanic/pull/2737) Fix type annotation for `JSONREsponse` default content type
- [#2740](https://github.com/sanic-org/sanic/pull/2740) Use Sanic's serializer for JSON responses in the Inspector
- [#2760](https://github.com/sanic-org/sanic/pull/2760) Support for `Request.get_current` in ASGI mode
- [#2773](https://github.com/sanic-org/sanic/pull/2773) Alow Blueprint routes to explicitly define error_format
- [#2774](https://github.com/sanic-org/sanic/pull/2774) Resolve headers on different renderers
- [#2782](https://github.com/sanic-org/sanic/pull/2782) Resolve pypy compatibility issues
### Deprecations and Removals
- [#2777](https://github.com/sanic-org/sanic/pull/2777) Remove Python 3.7 support
### Developer infrastructure
- [#2766](https://github.com/sanic-org/sanic/pull/2766) Unpin setuptools version
- [#2779](https://github.com/sanic-org/sanic/pull/2779) Run keep alive tests in loop to get available port
### Improved Documentation
- [#2741](https://github.com/sanic-org/sanic/pull/2741) Better documentation examples about running Sanic
From that list, the items to highlight in the release notes:

View File

@ -25,5 +25,5 @@ def key_exist_handler(request):
return text("num does not exist in request")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -50,5 +50,4 @@ def pop_handler(request):
app.blueprint(bp, url_prefix="/bp")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)

View File

@ -37,5 +37,4 @@ app.blueprint(blueprint)
app.blueprint(blueprint2)
app.blueprint(blueprint3)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=9999, debug=True)
app.run(host="0.0.0.0", port=9999, debug=True)

View File

@ -4,7 +4,6 @@ from sanic import Sanic, response
app = Sanic("DelayedResponseApp", strict_slashes=True)
app.config.AUTO_EXTEND = False
@app.get("/")
@ -12,7 +11,7 @@ async def handler(request):
return response.redirect("/sleep/3")
@app.get("/sleep/<t:float>")
@app.get("/sleep/<t:number>")
async def handler2(request, t=0.3):
await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n")

View File

@ -29,7 +29,7 @@ def proxy(request, path):
path=path,
_server=https.config.SERVER_NAME,
_external=True,
_scheme="https",
_scheme="http",
)
return response.redirect(url)
@ -69,5 +69,5 @@ async def runner(app: Sanic, app_server: AsyncioServer):
app.is_running = False
app.is_stopping = True
if __name__ == "__main__":
https.run(port=HTTPS_PORT, debug=True)
https.run(port=HTTPS_PORT, debug=True)

View File

@ -39,5 +39,4 @@ async def test(request):
return json(response)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, workers=2)
app.run(host="0.0.0.0", port=8000, workers=2)

View File

@ -20,5 +20,4 @@ def test(request):
return text("hey")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
app.run(host="0.0.0.0", port=8000)

View File

@ -6,5 +6,5 @@ data = ""
for i in range(1, 250000):
data += str(i)
r = requests.post("http://0.0.0.0:8000/stream", data=data)
r = requests.post('http://0.0.0.0:8000/stream', data=data)
print(r.text)

View File

@ -20,5 +20,4 @@ def timeout(request, exception):
return response.text("RequestTimeout from error_handler.", 408)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
app.run(host="0.0.0.0", port=8000)

View File

@ -35,34 +35,34 @@ async def after_server_stop(app, loop):
async def test(request):
return response.json({"answer": "42"})
if __name__ == "__main__":
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: AsyncioServer = loop.run_until_complete(serv_task)
loop.run_until_complete(server.startup())
# When using app.run(), this actually triggers before the serv_coro.
# But, in this example, we are using the convenience method, even if it is
# out of order.
loop.run_until_complete(server.before_start())
loop.run_until_complete(server.after_start())
try:
loop.run_forever()
except KeyboardInterrupt:
loop.stop()
finally:
loop.run_until_complete(server.before_stop())
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: AsyncioServer = loop.run_until_complete(serv_task)
loop.run_until_complete(server.startup())
# Wait for server to close
close_task = server.close()
loop.run_until_complete(close_task)
# When using app.run(), this actually triggers before the serv_coro.
# But, in this example, we are using the convenience method, even if it is
# out of order.
loop.run_until_complete(server.before_start())
loop.run_until_complete(server.after_start())
try:
loop.run_forever()
except KeyboardInterrupt:
loop.stop()
finally:
loop.run_until_complete(server.before_stop())
# Complete all tasks on the loop
for connection in server.connections:
connection.close_if_idle()
loop.run_until_complete(server.after_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()
loop.run_until_complete(server.after_stop())

View File

@ -1,3 +1,6 @@
import os
import socket
from sanic import Sanic, response
@ -10,4 +13,13 @@ async def test(request):
if __name__ == "__main__":
app.run(unix="./uds_socket")
server_address = "./uds_socket"
# Make sure the socket does not already exist
try:
os.unlink(server_address)
except OSError:
if os.path.exists(server_address):
raise
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_address)
app.run(sock=sock)

View File

@ -1 +0,0 @@
web: sanic --port=${PORT} --host=0.0.0.0 --workers=1 server:app

View File

@ -1 +0,0 @@
current_version: "23.6"

View File

@ -1,15 +0,0 @@
root:
- label: Home
path: index.html
- label: Community
items:
- label: Forums
href: https://community.sanicframework.org
- label: Discord
href: https://discord.gg/FARQzAEMAA
- label: Twitter
href: https://twitter.com/sanicframework
- label: Help
path: ./help.html
- label: GitHub
href: https://github.com/sanic-org/sanic

View File

@ -1,257 +0,0 @@
root:
- label: User Guide
items:
- label: General
items:
- label: Introduction
path: guide/introduction.html
- label: Getting Started
path: guide/getting-started.html
- label: Basics
items:
- label: Sanic Application
path: guide/basics/app.html
- label: Handlers
path: guide/basics/handlers.html
- label: Request
path: guide/basics/request.html
- label: Response
path: guide/basics/response.html
- label: Routing
path: guide/basics/routing.html
- label: Listeners
path: guide/basics/listeners.html
- label: Middleware
path: guide/basics/middleware.html
- label: Headers
path: guide/basics/headers.html
- label: Cookies
path: guide/basics/cookies.html
- label: Background Tasks
path: guide/basics/tasks.html
- label: Advanced
items:
- label: Class Based Views
path: guide/advanced/class-based-views.html
- label: Proxy Configuration
path: guide/advanced/proxy-headers.html
- label: Streaming
path: guide/advanced/streaming.html
- label: Websockets
path: guide/advanced/websockets.html
- label: Versioning
path: guide/advanced/versioning.html
- label: Signals
path: guide/advanced/signals.html
- label: Best Practices
items:
- label: Blueprints
path: guide/best-practices/blueprints.html
- label: Exceptions
path: guide/best-practices/exceptions.html
- label: Decorators
path: guide/best-practices/decorators.html
- label: Logging
path: guide/best-practices/logging.html
- label: Testing
path: guide/best-practices/testing.html
- label: Running Sanic
items:
- label: Configuration
path: guide/running/configuration.html
- label: Development
path: guide/running/development.html
- label: Server
path: guide/running/running.html
- label: Worker Manager
path: guide/running/manager.html
- label: Dynamic Applications
path: guide/running/app-loader.html
- label: Inspector
path: guide/running/inspector.html
- label: Deployment
items:
- label: Caddy
path: guide/deployment/caddy.html
- label: Nginx
path: guide/deployment/nginx.html
- label: Docker
path: guide/deployment/docker.html
- label: How to ...
items:
- label: Table of Contents
path: guide/how-to/table-of-contents.html
- label: Application Mounting
path: guide/how-to/mounting.html
- label: Authentication
path: guide/how-to/authentication.html
- label: Autodiscovery
path: guide/how-to/autodiscovery.html
- label: CORS
path: guide/how-to/cors.html
- label: ORM
path: guide/how-to/orm.html
- label: Static Redirects
path: guide/how-to/static-redirects.html
- label: TLS/SSL/HTTPS
path: guide/how-to/tls.html
- label: Plugins
items:
- label: Sanic Extensions
items:
- label: Getting Started
path: plugins/sanic-ext/getting-started.html
- label: HTTP - Methods
path: plugins/sanic-ext/http/methods.html
- label: HTTP - CORS Protection
path: plugins/sanic-ext/http/cors.html
- label: OpenAPI - Basics
path: plugins/sanic-ext/openapi/basics.html
- label: OpenAPI - UI
path: plugins/sanic-ext/openapi/ui.html
- label: OpenAPI - Decorators
path: plugins/sanic-ext/openapi/decorators.html
# - label: OpenAPI - Advanced
# path: plugins/sanic-ext/openapi/advanced.html
- label: OpenAPI - Auto Documentation
path: plugins/sanic-ext/openapi/autodoc.html
- label: OpenAPI - Security
path: plugins/sanic-ext/openapi/security.html
- label: Convenience
path: plugins/sanic-ext/convenience.html
- label: Templating - Jinja
path: plugins/sanic-ext/templating/jinja.html
- label: Templating - html5tagger
path: plugins/sanic-ext/templating/html5tagger.html
- label: Dependency Injection
path: plugins/sanic-ext/injection.html
- label: Validation
path: plugins/sanic-ext/validation.html
- label: Health Monitor
path: plugins/sanic-ext/health-monitor.html
- label: Background Logger
path: plugins/sanic-ext/logger.html
- label: Configuration
path: plugins/sanic-ext/configuration.html
- label: Custom Extensions
path: plugins/sanic-ext/custom.html
- label: Sanic Testing
items:
- label: Getting Started
path: plugins/sanic-testing/getting-started.html
- label: Test Clients
path: plugins/sanic-testing/clients.html
- label: Release Notes
items:
- label: "2023"
items:
- label: Sanic 23.6
path: release-notes/2023/v23.6.html
- label: Sanic 23.3
path: release-notes/2023/v23.3.html
- label: "2022"
items:
- label: Sanic 22.12
path: release-notes/2022/v22.12.html
- label: Sanic 22.9
path: release-notes/2022/v22.9.html
- label: Sanic 22.6
path: release-notes/2022/v22.6.html
- label: Sanic 22.3
path: release-notes/2022/v22.3.html
- label: "2021"
items:
- label: Sanic 21.12
path: release-notes/2021/v21.12.html
- label: Sanic 21.9
path: release-notes/2021/v21.9.html
- label: Sanic 21.6
path: release-notes/2021/v21.6.html
- label: Sanic 21.3
path: release-notes/2021/v21.3.html
- label: Organization
items:
- label: Contributing
path: organization/contributing.html
- label: Code of Conduct
path: organization/code-of-conduct.html
- label: S.C.O.P.E. (Governance)
path: organization/scope.html
- label: Policies
path: organization/policies.html
- label: API Reference
items:
- label: Application
items:
- label: sanic.app
path: /api/sanic.app.html
- label: sanic.config
path: /api/sanic.config.html
- label: sanic.application
path: /api/sanic.application.html
- label: Blueprint
items:
- label: sanic.blueprints
path: /api/sanic.blueprints.html
- label: sanic.blueprint_group
path: /api/sanic.blueprint_group.html
- label: Constant
items:
- label: sanic.constants
path: /api/sanic.constants.html
- label: Core
items:
- label: sanic.cookies
path: /api/sanic.cookies.html
- label: sanic.handlers
path: /api/sanic.handlers.html
- label: sanic.headers
path: /api/sanic.headers.html
- label: sanic.middleware
path: /api/sanic.middleware.html
- label: sanic.mixins
path: /api/sanic.mixins.html
- label: sanic.request
path: /api/sanic.request.html
- label: sanic.response
path: /api/sanic.response.html
- label: sanic.views
path: /api/sanic.views.html
- label: Display
items:
- label: sanic.pages
path: /api/sanic.pages.html
- label: Exception
items:
- label: sanic.errorpages
path: /api/sanic.errorpages.html
- label: sanic.exceptions
path: /api/sanic.exceptions.html
- label: Model
items:
- label: sanic.models
path: /api/sanic.models.html
- label: Routing
items:
- label: sanic.router
path: /api/sanic.router.html
- label: sanic.signals
path: /api/sanic.signals.html
- label: Server
items:
- label: sanic.http
path: /api/sanic.http.html
- label: sanic.server
path: /api/sanic.server.html
- label: sanic.worker
path: /api/sanic.worker.html
- label: Utility
items:
- label: sanic.compat
path: /api/sanic.compat.html
- label: sanic.helpers
path: /api/sanic.helpers.html
- label: sanic.log
path: /api/sanic.log.html
- label: sanic.utils
path: /api/sanic.utils.html

File diff suppressed because it is too large Load Diff

View File

@ -1,202 +0,0 @@
# Class Based Views
## Why use them?
.. column::
### The problem
A common pattern when designing an API is to have multiple functionality on the same endpoint that depends upon the HTTP method.
While both of these options work, they are not good design practices and may be hard to maintain over time as your project grows.
.. column::
```python
@app.get("/foo")
async def foo_get(request):
...
@app.post("/foo")
async def foo_post(request):
...
@app.put("/foo")
async def foo_put(request):
...
@app.route("/bar", methods=["GET", "POST", "PATCH"])
async def bar(request):
if request.method == "GET":
...
elif request.method == "POST":
...
elif request.method == "PATCH":
...
```
.. column::
### The solution
Class-based views are simply classes that implement response behavior to requests. They provide a way to compartmentalize handling of different HTTP request types at the same endpoint.
.. column::
```python
from sanic.views import HTTPMethodView
class FooBar(HTTPMethodView):
async def get(self, request):
...
async def post(self, request):
...
async def put(self, request):
...
app.add_route(FooBar.as_view(), "/foobar")
```
## Defining a view
A class-based view should subclass `HTTPMethodView`. You can then implement class methods with the name of the corresponding HTTP method. If a request is received that has no defined method, a `405: Method not allowed` response will be generated.
.. column::
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
- delete
- head
- options
.. column::
```python
from sanic.views import HTTPMethodView
from sanic.response import text
class SimpleView(HTTPMethodView):
def get(self, request):
return text("I am get method")
# You can also use async syntax
async def post(self, request):
return text("I am post method")
def put(self, request):
return text("I am put method")
def patch(self, request):
return text("I am patch method")
def delete(self, request):
return text("I am delete method")
app.add_route(SimpleView.as_view(), "/")
```
## Path parameters
.. column::
You can use path parameters exactly as discussed in [the routing section](/guide/basics/routing.md).
.. column::
```python
class NameView(HTTPMethodView):
def get(self, request, name):
return text("Hello {}".format(name))
app.add_route(NameView.as_view(), "/<name>")
```
## Decorators
As discussed in [the decorators section](/guide/best-practices/decorators.md), often you will need to add functionality to endpoints with the use of decorators. You have two options with CBV:
1. Apply to _all_ HTTP methods in the view
2. Apply individually to HTTP methods in the view
Let's see what the options look like:
.. column::
### Apply to all methods
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.
.. column::
```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")
```
.. column::
### Apply to individual methods
But if you just want to decorate some methods and not all methods, you can as shown here.
.. column::
```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 do not have any decorators")
@some_decorator_here
def patch(self, request, name):
return text("Hello I have a decorator")
```
## Generating a URL
.. column::
This works just like [generating any other URL](/guide/basics/routing.md#generating-a-url), except that the class name is a part of the endpoint.
.. column::
```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")
```

View File

@ -1,477 +0,0 @@
# Proxy configuration
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain the IP of a proxy, typically `127.0.0.1`. Almost always, this is **not** what you will want.
Sanic may be configured to use proxy headers for determining the true client IP, available as `request.remote_addr`. The full external URL is also constructed from header fields _if available_.
.. tip:: Heads up
Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled.
.. column::
Services behind reverse proxies must configure one or more of the following [configuration values](/guide/deployment/configuration.md):
- `FORWARDED_SECRET`
- `REAL_IP_HEADER`
- `PROXIES_COUNT`
.. column::
```python
app.config.FORWARDED_SECRET = "super-duper-secret"
app.config.REAL_IP_HEADER = "CF-Connecting-IP"
app.config.PROXIES_COUNT = 2
```
## Forwarded header
In order to use the `Forwarded` header, you should set `app.config.FORWARDED_SECRET` to a value known to the trusted proxy server. The secret is used to securely identify a specific proxy server.
Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set.
All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client.
To learn more about the `Forwarded` header, read the related [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded) and [Nginx](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/) articles.
## Traditional proxy headers
### IP Headers
When your proxy forwards you the IP address in a known header, you can tell Sanic what that is with the `REAL_IP_HEADER` config value.
### X-Forwarded-For
This header typically contains a chain of IP addresses through each layer of a proxy. Setting `PROXIES_COUNT` tells Sanic how deep to look to get an actual IP address for the client. This value should equal the _expected_ number of IP addresses in the chain.
### Other X-headers
If a client IP is found by one of these methods, Sanic uses the following headers for URL parts:
- x-forwarded-proto
- x-forwarded-host
- x-forwarded-port
- x-forwarded-path
- x-scheme
## Examples
In the following examples, all requests will assume that the endpoint looks like this:
```python
@app.route("/fwd")
async def forwarded(request):
return json(
{
"remote_addr": request.remote_addr,
"scheme": request.scheme,
"server_name": request.server_name,
"server_port": request.server_port,
"forwarded": request.forwarded,
}
)
```
.. column::
---
##### Example 1
Without configured FORWARDED_SECRET, x-headers should be respected
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: for=1.1.1.1, for=injected;host=", for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret,for=broken;;secret=b0rked, for=127.0.0.3;scheme=http;port=1234' \
-H "X-Real-IP: 127.0.0.2" \
-H "X-Forwarded-For: 127.0.1.1" \
-H "X-Scheme: ws" \
-H "Host: local.site" | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "127.0.0.2",
"scheme": "ws",
"server_name": "local.site",
"server_port": 80,
"forwarded": {
"for": "127.0.0.2",
"proto": "ws"
}
}
```
---
.. column::
##### Example 2
FORWARDED_SECRET now configured
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: for=1.1.1.1, for=injected;host=", for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret,for=broken;;secret=b0rked, for=127.0.0.3;scheme=http;port=1234' \
-H "X-Real-IP: 127.0.0.2" \
-H "X-Forwarded-For: 127.0.1.1" \
-H "X-Scheme: ws" \
-H "Host: local.site" | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "[::2]",
"scheme": "https",
"server_name": "me.tld",
"server_port": 443,
"forwarded": {
"for": "[::2]",
"proto": "https",
"host": "me.tld",
"path": "/app/",
"secret": "mySecret"
}
}
```
---
.. column::
##### Example 3
Empty Forwarded header -> use X-headers
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H "X-Real-IP: 127.0.0.2" \
-H "X-Forwarded-For: 127.0.1.1" \
-H "X-Scheme: ws" \
-H "Host: local.site" | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "127.0.0.2",
"scheme": "ws",
"server_name": "local.site",
"server_port": 80,
"forwarded": {
"for": "127.0.0.2",
"proto": "ws"
}
}
```
---
.. column::
##### Example 4
Header present but not matching anything
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H "Forwarded: nomatch" | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "",
"scheme": "http",
"server_name": "localhost",
"server_port": 8000,
"forwarded": {}
}
```
---
.. column::
##### Example 5
Forwarded header present but no matching secret -> use X-headers
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H "Forwarded: for=1.1.1.1;secret=x, for=127.0.0.1" \
-H "X-Real-IP: 127.0.0.2" | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "127.0.0.2",
"scheme": "http",
"server_name": "localhost",
"server_port": 8000,
"forwarded": {
"for": "127.0.0.2"
}
}
```
---
.. column::
##### Example 6
Different formatting and hitting both ends of the header
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: Secret="mySecret";For=127.0.0.4;Port=1234' | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "127.0.0.4",
"scheme": "http",
"server_name": "localhost",
"server_port": 1234,
"forwarded": {
"secret": "mySecret",
"for": "127.0.0.4",
"port": 1234
}
}
```
---
.. column::
##### Example 7
Test escapes (modify this if you see anyone implementing quoted-pairs)
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: for=test;quoted="\,x=x;y=\";secret=mySecret' | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "test",
"scheme": "http",
"server_name": "localhost",
"server_port": 8000,
"forwarded": {
"for": "test",
"quoted": "\\,x=x;y=\\",
"secret": "mySecret"
}
}
```
---
.. column::
##### Example 8
Secret insulated by malformed field #1
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: for=test;secret=mySecret;b0rked;proto=wss;' | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "test",
"scheme": "http",
"server_name": "localhost",
"server_port": 8000,
"forwarded": {
"for": "test",
"secret": "mySecret"
}
}
```
---
.. column::
##### Example 9
Secret insulated by malformed field #2
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: for=test;b0rked;secret=mySecret;proto=wss' | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "",
"scheme": "wss",
"server_name": "localhost",
"server_port": 8000,
"forwarded": {
"secret": "mySecret",
"proto": "wss"
}
}
```
---
.. column::
##### Example 10
Unexpected termination should not lose existing acceptable values
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: b0rked;secret=mySecret;proto=wss' | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "",
"scheme": "wss",
"server_name": "localhost",
"server_port": 8000,
"forwarded": {
"secret": "mySecret",
"proto": "wss"
}
}
```
---
.. column::
##### Example 11
Field normalization
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "mySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: PROTO=WSS;BY="CAFE::8000";FOR=unknown;PORT=X;HOST="A:2";PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "",
"scheme": "wss",
"server_name": "a",
"server_port": 2,
"forwarded": {
"proto": "wss",
"by": "[cafe::8000]",
"host": "a:2",
"path": "/With Spaces\"Quoted\"/sanicApp?key=val",
"secret": "mySecret"
}
}
```
---
.. column::
##### Example 12
Using "by" field as secret
```python
app.config.PROXIES_COUNT = 1
app.config.REAL_IP_HEADER = "x-real-ip"
app.config.FORWARDED_SECRET = "_proxySecret"
```
```bash
$ curl localhost:8000/fwd \
-H 'Forwarded: for=1.2.3.4; by=_proxySecret' | jq
```
.. column::
```bash
# curl response
{
"remote_addr": "1.2.3.4",
"scheme": "http",
"server_name": "localhost",
"server_port": 8000,
"forwarded": {
"for": "1.2.3.4",
"by": "_proxySecret"
}
}
```

View File

@ -1,346 +0,0 @@
# Signals
Signals provide a way for one part of your application to tell another part that something happened.
```python
@app.signal("user.registration.created")
async def send_registration_email(**context):
await send_email(context["email"], template="registration")
@app.post("/register")
async def handle_registration(request):
await do_registration(request)
await request.app.dispatch(
"user.registration.created",
context={"email": request.json.email}
})
```
## Adding a signal
.. column::
The API for adding a signal is very similar to adding a route.
.. column::
```python
async def my_signal_handler():
print("something happened")
app.add_signal(my_signal_handler, "something.happened.ohmy")
```
.. column::
But, perhaps a slightly more convenient method is to use the built-in decorators.
.. column::
```python
@app.signal("something.happened.ohmy")
async def my_signal_handler():
print("something happened")
```
.. column::
If the signal requires conditions, make sure to add them while adding the handler.
.. column::
```python
async def my_signal_handler1():
print("something happened")
app.add_signal(
my_signal_handler,
"something.happened.ohmy1",
conditions={"some_condition": "value"}
)
@app.signal("something.happened.ohmy2", conditions={"some_condition": "value"})
async def my_signal_handler2():
print("something happened")
```
.. column::
Signals can also be declared on blueprints
.. column::
```python
bp = Blueprint("foo")
@bp.signal("something.happened.ohmy")
async def my_signal_handler():
print("something happened")
```
## Built-in signals
In addition to creating a new signal, there are a number of built-in signals that are dispatched from Sanic itself. These signals exist to provide developers with more opportunities to add functionality into the request and server lifecycles.
*Added in v21.9*
.. column::
You can attach them just like any other signal to an application or blueprint instance.
.. column::
```python
@app.signal("http.lifecycle.complete")
async def my_signal_handler(conn_info):
print("Connection has been closed")
```
These signals are the signals that are available, along with the arguments that the handlers take, and the conditions that attach (if any).
| Event name | Arguments | Conditions |
| -------------------------- | ------------------------------- | --------------------------------------------------------- |
| `http.routing.before` | request | |
| `http.routing.after` | request, route, kwargs, handler | |
| `http.handler.before` | request | |
| `http.handler.after` | request | |
| `http.lifecycle.begin` | conn_info | |
| `http.lifecycle.read_head` | head | |
| `http.lifecycle.request` | request | |
| `http.lifecycle.handle` | request | |
| `http.lifecycle.read_body` | body | |
| `http.lifecycle.exception` | request, exception | |
| `http.lifecycle.response` | request, response | |
| `http.lifecycle.send` | data | |
| `http.lifecycle.complete` | conn_info | |
| `http.middleware.before` | request, response | `{"attach_to": "request"}` or `{"attach_to": "response"}` |
| `http.middleware.after` | request, response | `{"attach_to": "request"}` or `{"attach_to": "response"}` |
| `server.exception.report` | app, exception | |
| `server.init.before` | app, loop | |
| `server.init.after` | app, loop | |
| `server.shutdown.before` | app, loop | |
| `server.shutdown.after` | app, loop | |
Version 22.9 added `http.handler.before` and `http.handler.after`.
Version 23.6 added `server.exception.report`.
.. column::
To make using the built-in signals easier, there is an `Enum` object that contains all of the allowed built-ins. With a modern IDE this will help so that you do not need to remember the full list of event names as strings.
*Added in v21.12*
.. column::
```python
from sanic.signals import Event
@app.signal(Event.HTTP_LIFECYCLE_COMPLETE)
async def my_signal_handler(conn_info):
print("Connection has been closed")
```
## Events
.. column::
Signals are based off of an _event_. An event, is simply a string in the following pattern:
.. column::
```
namespace.reference.action
```
.. tip:: Events must have three parts. If you do not know what to use, try these patterns:
- `my_app.something.happened`
- `sanic.notice.hello`
### Event parameters
.. column::
An event can be "dynamic" and declared using the same syntax as [path parameters](../basics/routing.md#path-parameters). This allows matching based upon arbitrary values.
.. column::
```python
@app.signal("foo.bar.<thing>")
async def signal_handler(thing):
print(f"[signal_handler] {thing=}")
@app.get("/")
async def trigger(request):
await app.dispatch("foo.bar.baz")
return response.text("Done.")
```
Checkout [path parameters](../basics/routing.md#path-parameters) for more information on allowed type definitions.
.. warning:: Only the third part of an event (the action) may be dynamic:
- `foo.bar.<thing>` 🆗
- `foo.<bar>.baz`
### Waiting
.. column::
In addition to executing a signal handler, your application can wait for an event to be triggered.
.. column::
```python
await app.event("foo.bar.baz")
```
.. column::
**IMPORTANT**: waiting is a blocking function. Therefore, you likely will want this to run in a [background task](../basics/tasks.md).
.. column::
```python
async def wait_for_event(app):
while True:
print("> waiting")
await app.event("foo.bar.baz")
print("> event found\n")
@app.after_server_start
async def after_server_start(app, loop):
app.add_task(wait_for_event(app))
```
.. column::
If your event was defined with a dynamic path, you can use `*` to catch any action.
.. column::
```python
@app.signal("foo.bar.<thing>")
...
await app.event("foo.bar.*")
```
## Dispatching
*In the future, Sanic will dispatch some events automatically to assist developers to hook into life cycle events.*
.. column::
Dispatching an event will do two things:
1. execute any signal handlers defined on the event, and
2. resolve anything that is "waiting" for the event to complete.
.. column::
```python
@app.signal("foo.bar.<thing>")
async def foo_bar(thing):
print(f"{thing=}")
await app.dispatch("foo.bar.baz")
```
```
thing=baz
```
### Context
.. column::
Sometimes you may find the need to pass extra information into the signal handler. In our first example above, we wanted our email registration process to have the email address for the user.
.. column::
```python
@app.signal("user.registration.created")
async def send_registration_email(**context):
print(context)
await app.dispatch(
"user.registration.created",
context={"hello": "world"}
)
```
```
{'hello': 'world'}
```
.. tip:: FYI
Signals are dispatched in a background task.
### Blueprints
Dispatching blueprint signals works similar in concept to [middleware](../basics/middleware.md). Anything that is done from the app level, will trickle down to the blueprints. However, dispatching on a blueprint, will only execute the signals that are defined on that blueprint.
.. column::
Perhaps an example is easier to explain:
.. column::
```python
bp = Blueprint("bp")
app_counter = 0
bp_counter = 0
@app.signal("foo.bar.baz")
def app_signal():
nonlocal app_counter
app_counter += 1
@bp.signal("foo.bar.baz")
def bp_signal():
nonlocal bp_counter
bp_counter += 1
```
.. column::
Running `app.dispatch("foo.bar.baz")` will execute both signals.
.. column::
```python
await app.dispatch("foo.bar.baz")
assert app_counter == 1
assert bp_counter == 1
```
.. column::
Running `bp.dispatch("foo.bar.baz")` will execute only the blueprint signal.
.. column::
```python
await bp.dispatch("foo.bar.baz")
assert app_counter == 1
assert bp_counter == 2
```

View File

@ -1,151 +0,0 @@
# Streaming
## Request streaming
Sanic allows you to stream data sent by the client to begin processing data as the bytes arrive.
.. column::
When enabled on an endpoint, you can stream the request body using `await request.stream.read()`.
That method will return `None` when the body is completed.
.. column::
```python
from sanic.views import stream
class SimpleView(HTTPMethodView):
@stream
async def post(self, request):
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
```
.. column::
It also can be enabled with a keyword argument in the decorator...
.. column::
```python
@app.post("/stream", stream=True)
async def handler(request):
...
body = await request.stream.read()
...
```
.. column::
... or the `add_route()` method.
.. column::
```python
bp.add_route(
bp_handler,
"/bp_stream",
methods=["POST"],
stream=True,
)
```
.. tip:: FYI
Only post, put and patch decorators have stream argument.
## Response streaming
.. column::
Sanic allows you to stream content to the client.
.. column::
```python
@app.route("/")
async def test(request):
response = await request.respond(content_type="text/csv")
await response.send("foo,")
await response.send("bar")
# Optionally, you can explicitly end the stream by calling:
await response.eof()
```
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):
response = await request.respond()
conn = await asyncpg.connect(database='test')
async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'):
await response.send(record[0])
```
You can explicitly end a stream by calling `await response.eof()`. It a convenience method to replace `await response.send("", True)`. It should be called **one time** *after* your handler has determined that it has nothing left to send back to the client. While it is *optional* to use with Sanic server, if you are running Sanic in ASGI mode, then you **must** explicitly terminate the stream.
*Calling `eof` became optional in v21.6*
## File streaming
.. column::
Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesnt add `Content-Length` HTTP header in the response.
A typical use case might be streaming an video file.
.. column::
```python
@app.route("/mp4")
async def handler_file_stream(request):
return await response.file_stream(
"/path/to/sample.mp4",
chunk_size=1024,
mime_type="application/metalink4+xml",
headers={
"Content-Disposition": 'Attachment; filename="nicer_name.meta4"',
"Content-Type": "application/metalink4+xml",
},
)
```
.. column::
If you want to use the `Content-Length` header, you can disable chunked transfer encoding and add it manually simply by adding the `Content-Length` header.
.. column::
```python
from aiofiles import os as async_os
from sanic.response import file_stream
@app.route("/")
async def index(request):
file_path = "/srv/www/whatever.png"
file_stat = await async_os.stat(file_path)
headers = {"Content-Length": str(file_stat.st_size)}
return await file_stream(
file_path,
headers=headers,
)
```

View File

@ -1,170 +0,0 @@
# Versioning
It is standard practice in API building to add versions to your endpoints. This allows you to easily differentiate incompatible endpoints when you try and change your API down the road in a breaking manner.
Adding a version will add a `/v{version}` url prefix to your endpoints.
The version can be a `int`, `float`, or `str`. Acceptable values:
- `1`, `2`, `3`
- `1.1`, `2.25`, `3.0`
- `"1"`, `"v1"`, `"v1.1"`
## Per route
.. column::
You can pass a version number to the routes directly.
.. column::
```python
# /v1/text
@app.route("/text", version=1)
def handle_request(request):
return response.text("Hello world! Version 1")
# /v2/text
@app.route("/text", version=2)
def handle_request(request):
return response.text("Hello world! Version 2")
```
## Per Blueprint
.. column::
You can also pass a version number to the blueprint, which will apply to all routes in that blueprint.
.. column::
```python
bp = Blueprint("test", url_prefix="/foo", version=1)
# /v1/foo/html
@bp.route("/html")
def handle_request(request):
return response.html("<p>Hello world!</p>")
```
## Per Blueprint Group
.. column::
In order to simplify the management of the versioned blueprints, you can provide a version number in the blueprint
group. The same will be inherited to all the blueprint grouped under it if the blueprints don't already override the
same information with a value specified while creating a blueprint instance.
When using blueprint groups for managing the versions, the following order is followed to apply the Version prefix to
the routes being registered.
1. Route Level configuration
2. Blueprint level configuration
3. Blueprint Group level configuration
If we find a more pointed versioning specification, we will pick that over the more generic versioning specification
provided under the Blueprint or Blueprint Group
.. column::
```python
from sanic.blueprints import Blueprint
from sanic.response import json
bp1 = Blueprint(
name="blueprint-1",
url_prefix="/bp1",
version=1.25,
)
bp2 = Blueprint(
name="blueprint-2",
url_prefix="/bp2",
)
group = Blueprint.group(
[bp1, bp2],
url_prefix="/bp-group",
version="v2",
)
# GET /v1.25/bp-group/bp1/endpoint-1
@bp1.get("/endpoint-1")
async def handle_endpoint_1_bp1(request):
return json({"Source": "blueprint-1/endpoint-1"})
# GET /v2/bp-group/bp2/endpoint-2
@bp2.get("/endpoint-1")
async def handle_endpoint_1_bp2(request):
return json({"Source": "blueprint-2/endpoint-1"})
# GET /v1/bp-group/bp2/endpoint-2
@bp2.get("/endpoint-2", version=1)
async def handle_endpoint_2_bp2(request):
return json({"Source": "blueprint-2/endpoint-2"})
```
## Version prefix
As seen above, the `version` that is applied to a route is **always** the first segment in the generated URI path. Therefore, to make it possible to add path segments before the version, every place that a `version` argument is passed, you can also pass `version_prefix`.
The `version_prefix` argument can be defined in:
- `app.route` and `bp.route` decorators (and all the convenience decorators also)
- `Blueprint` instantiation
- `Blueprint.group` constructor
- `BlueprintGroup` instantiation
- `app.blueprint` registration
If there are definitions in multiple places, a more specific definition overrides a more general. This list provides that hierarchy.
The default value of `version_prefix` is `/v`.
.. column::
An often requested feature is to be able to mount versioned routes on `/api`. This can easily be accomplished with `version_prefix`.
.. column::
```python
# /v1/my/path
app.route("/my/path", version=1, version_prefix="/api/v")
```
.. column::
Perhaps a more compelling usage is to load all `/api` routes into a single `BlueprintGroup`.
.. column::
```python
# /v1/my/path
app = Sanic(__name__)
v2ip = Blueprint("v2ip", url_prefix="/ip", version=2)
api = Blueprint.group(v2ip, version_prefix="/api/version")
# /api/version2/ip
@v2ip.get("/")
async def handler(request):
return text(request.ip)
app.blueprint(api)
```
We can therefore learn that a route's URI is:
```
version_prefix + version + url_prefix + URI definition
```
.. tip::
Just like with `url_prefix`, it is possible to define path parameters inside a `version_prefix`. It is perfectly legitimate to do this. Just remember that every route will have that parameter injected into the handler.
```python
version_prefix="/<foo:str>/v"
```
*Added in v21.6*

View File

@ -1,82 +0,0 @@
# Websockets
Sanic provides an easy to use abstraction on top of [websockets](https://websockets.readthedocs.io/en/stable/).
## Routing
.. column::
Websocket handlers can be hooked up to the router similar to regular handlers.
.. column::
```python
from sanic import Request, Websocket
async def feed(request: Request, ws: Websocket):
pass
app.add_websocket_route(feed, "/feed")
```
```python
from sanic import Request, Websocket
@app.websocket("/feed")
async def feed(request: Request, ws: Websocket):
pass
```
## Handler
.. column::
Typically, a websocket handler will want to hold open a loop.
It can then use the `send()` and `recv()` methods on the second object injected into the handler.
This example is a simple endpoint that echos back to the client messages that it receives.
.. column::
```python
from sanic import Request, Websocket
@app.websocket("/feed")
async def feed(request: Request, ws: Websocket):
while True:
data = "hello!"
print("Sending: " + data)
await ws.send(data)
data = await ws.recv()
print("Received: " + data)
```
.. column::
You can simplify your loop by just iterating over the `Websocket` object in a for loop.
*Added in v22.9*
.. column::
```python
from sanic import Request, Websocket
@app.websocket("/feed")
async def feed(request: Request, ws: Websocket):
async for msg in ws:
await ws.send(msg)
```
## Configuration
See [configuration section](/guide/deployment/configuration.md) for more details, however the defaults are shown below.
```python
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
app.config.WEBSOCKET_PING_INTERVAL = 20
app.config.WEBSOCKET_PING_TIMEOUT = 20
```

View File

@ -1 +0,0 @@
# Basics

View File

@ -1,517 +0,0 @@
# Sanic Application
## Instance
.. column::
The most basic building block is the `Sanic()` instance. It is not required, but the custom is to instantiate this in a file called `server.py`.
.. column::
```python
# /path/to/server.py
from sanic import Sanic
app = Sanic("MyHelloWorldApp")
```
## Application context
Most applications will have the need to share/reuse data or objects across different parts of the code base. The most common example is DB connections.
.. column::
In versions of Sanic prior to v21.3, this was commonly done by attaching an attribute to the application instance
.. column::
```python
# Raises a warning as deprecated feature in 21.3
app = Sanic("MyApp")
app.db = Database()
```
.. column::
Because this can create potential problems with name conflicts, and to be consistent with [request context](./request.md#context) objects, v21.3 introduces application level context object.
.. column::
```python
# Correct way to attach objects to the application
app = Sanic("MyApp")
app.ctx.db = Database()
```
## App Registry
.. column::
When you instantiate a Sanic instance, that can be retrieved at a later time from the Sanic app registry. This can be useful, for example, if you need to access your Sanic instance from a location where it is not otherwise accessible.
.. column::
```python
# ./path/to/server.py
from sanic import Sanic
app = Sanic("my_awesome_server")
# ./path/to/somewhere_else.py
from sanic import Sanic
app = Sanic.get_app("my_awesome_server")
```
.. column::
If you call `Sanic.get_app("non-existing")` on an app that does not exist, it will raise `SanicException` by default. You can, instead, force the method to return a new instance of Sanic with that name.
.. column::
```python
app = Sanic.get_app(
"non-existing",
force_create=True,
)
```
.. column::
If there is **only one** Sanic instance registered, then calling `Sanic.get_app()` with no arguments will return that instance
.. column::
```python
Sanic("My only app")
app = Sanic.get_app()
```
## Configuration
.. column::
Sanic holds the configuration in the `config` attribute of the `Sanic` instance. Configuration can be modified **either** using dot-notation **OR** like a dictionary.
.. column::
```python
app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config['DB_USER'] = 'appuser'
db_settings = {
'DB_HOST': 'localhost',
'DB_NAME': 'appdb',
'DB_USER': 'appuser'
}
app.config.update(db_settings)
```
.. note:: Heads up
Config keys _should_ be uppercase. But, this is mainly by convention, and lowercase will work most of the time.
```
app.config.GOOD = "yay!"
app.config.bad = "boo"
```
There is much [more detail about configuration](/guide/deployment/configuration.md) later on.
## Customization
The Sanic application instance can be customized for your application needs in a variety of ways at instantiation.
### Custom configuration
.. column::
This simplest form of custom configuration would be to pass your own object directly into that Sanic application instance
If you create a custom configuration object, it is *highly* recommended that you subclass the Sanic `Config` option to inherit its behavior. You could use this option for adding properties, or your own set of custom logic.
*Added in v21.6*
.. column::
```python
from sanic.config import Config
class MyConfig(Config):
FOO = "bar"
app = Sanic(..., config=MyConfig())
```
.. column::
A useful example of this feature would be if you wanted to use a config file in a form that differs from what is [supported](../deployment/configuration.md#using-sanic-update-config).
.. column::
```python
from sanic import Sanic, text
from sanic.config import Config
class TomlConfig(Config):
def __init__(self, *args, path: str, **kwargs):
super().__init__(*args, **kwargs)
with open(path, "r") as f:
self.apply(toml.load(f))
def apply(self, config):
self.update(self._to_uppercase(config))
def _to_uppercase(self, obj: Dict[str, Any]) -> Dict[str, Any]:
retval: Dict[str, Any] = {}
for key, value in obj.items():
upper_key = key.upper()
if isinstance(value, list):
retval[upper_key] = [
self._to_uppercase(item) for item in value
]
elif isinstance(value, dict):
retval[upper_key] = self._to_uppercase(value)
else:
retval[upper_key] = value
return retval
toml_config = TomlConfig(path="/path/to/config.toml")
app = Sanic(toml_config.APP_NAME, config=toml_config)
```
### Custom context
.. column::
By default, the application context is a [`SimpleNamespace()`](https://docs.python.org/3/library/types.html#types.SimpleNamespace) that allows you to set any properties you want on it. However, you also have the option of passing any object whatsoever instead.
*Added in v21.6*
.. column::
```python
app = Sanic(..., ctx=1)
```
```python
app = Sanic(..., ctx={})
```
```python
class MyContext:
...
app = Sanic(..., ctx=MyContext())
```
### Custom requests
.. column::
It is sometimes helpful to have your own `Request` class, and tell Sanic to use that instead of the default. One example is if you wanted to modify the default `request.id` generator.
.. note:: Important
It is important to remember that you are passing the *class* not an instance of the class.
.. column::
```python
import time
from sanic import Request, Sanic, text
class NanoSecondRequest(Request):
@classmethod
def generate_id(*_):
return time.time_ns()
app = Sanic(..., request_class=NanoSecondRequest)
@app.get("/")
async def handler(request):
return text(str(request.id))
```
### Custom error handler
.. column::
See [exception handling](../best-practices/exceptions.md#custom-error-handling) for more
.. column::
```python
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(..., error_handler=CustomErrorHandler())
```
### Custom dumps function
.. column::
It may sometimes be necessary or desirable to provide a custom function that serializes an object to JSON data.
.. column::
```python
import ujson
dumps = partial(ujson.dumps, escape_forward_slashes=False)
app = Sanic(__name__, dumps=dumps)
```
.. column::
Or, perhaps use another library or create your own.
.. column::
```python
from orjson import dumps
app = Sanic(__name__, dumps=dumps)
```
### Custom loads function
.. column::
Similar to `dumps`, you can also provide a custom function for deserializing data.
*Added in v22.9*
.. column::
```python
from orjson import loads
app = Sanic(__name__, loads=loads)
```
.. new:: NEW in v23.6
### Custom typed application
The correct, default type of a Sanic application instance is:
```python
sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]
```
It refers to two generic types:
1. The first is the type of the configuration object. It defaults to `sanic.config.Config`, but can be any subclass of that.
2. The second is the type of the application context. It defaults to `types.SimpleNamespace`, but can be **any object** as show above.
Let's look at some examples of how the type will change.
.. column::
Consider this example where we pass a custom subclass of `Config` and a custom context object.
.. column::
```python
from sanic import Sanic
from sanic.config import Config
class CustomConfig(Config):
pass
app = Sanic("test", config=CustomConfig())
reveal_type(app) # N: Revealed type is "sanic.app.Sanic[main.CustomConfig, types.SimpleNamespace]"
```
```
sanic.app.Sanic[main.CustomConfig, types.SimpleNamespace]
```
.. column::
Similarly, when passing a custom context object, the type will change to reflect that.
.. column::
```python
from sanic import Sanic
class Foo:
pass
app = Sanic("test", ctx=Foo())
reveal_type(app) # N: Revealed type is "sanic.app.Sanic[sanic.config.Config, main.Foo]"
```
```
sanic.app.Sanic[sanic.config.Config, main.Foo]
```
.. column::
Of course, you can set both the config and context to custom types.
.. column::
```python
from sanic import Sanic
from sanic.config import Config
class CustomConfig(Config):
pass
class Foo:
pass
app = Sanic("test", config=CustomConfig(), ctx=Foo())
reveal_type(app) # N: Revealed type is "sanic.app.Sanic[main.CustomConfig, main.Foo]"
```
```
sanic.app.Sanic[main.CustomConfig, main.Foo]
```
This pattern is particularly useful if you create a custom type alias for your application instance so that you can use it to annotate listeners and handlers.
```python
# ./path/to/types.py
from sanic.app import Sanic
from sanic.config import Config
from myapp.context import MyContext
from typing import TypeAlias
MyApp = TypeAlias("MyApp", Sanic[Config, MyContext])
```
```python
# ./path/to/listeners.py
from myapp.types import MyApp
def add_listeners(app: MyApp):
@app.before_server_start
async def before_server_start(app: MyApp):
# do something with your fully typed app instance
await app.ctx.db.connect()
```
```python
# ./path/to/server.py
from myapp.types import MyApp
from myapp.context import MyContext
from myapp.config import MyConfig
from myapp.listeners import add_listeners
app = Sanic("myapp", config=MyConfig(), ctx=MyContext())
add_listeners(app)
```
*Added in v23.6*
### Custom typed request
Sanic also allows you to customize the type of the request object. This is useful if you want to add custom properties to the request object, or be able to access your custom properties of a typed application instance.
The correct, default type of a Sanic request instance is:
```python
sanic.request.Request[
sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace],
types.SimpleNamespace
]
```
It refers to two generic types:
1. The first is the type of the application instance. It defaults to `sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]`, but can be any subclass of that.
2. The second is the type of the request context. It defaults to `types.SimpleNamespace`, but can be **any object** as show above in [custom requests](#custom-requests).
Let's look at some examples of how the type will change.
.. column::
Expanding upon the full example above where there is a type alias for a customized application instance, we can also create a custom request type so that we can access those same type annotations.
Of course, you do not need type aliases for this to work. We are only showing them here to cut down on the amount of code shown.
.. column::
```python
from sanic import Request
from myapp.types import MyApp
from types import SimpleNamespace
def add_routes(app: MyApp):
@app.get("/")
async def handler(request: Request[MyApp, SimpleNamespace]):
# do something with your fully typed app instance
results = await request.app.ctx.db.query("SELECT * FROM foo")
```
.. column::
Perhaps you have a custom request object that generates a custom context object. You can type annotate it to properly access those properties with your IDE as shown here.
.. column::
```python
from sanic import Request, Sanic
from sanic.config import Config
class CustomConfig(Config):
pass
class Foo:
pass
class RequestContext:
foo: Foo
class CustomRequest(Request[Sanic[CustomConfig, Foo], RequestContext]):
@staticmethod
def make_context() -> RequestContext:
ctx = RequestContext()
ctx.foo = Foo()
return ctx
app = Sanic(
"test", config=CustomConfig(), ctx=Foo(), request_class=CustomRequest
)
@app.get("/")
async def handler(request: CustomRequest):
# Full access to typed:
# - custom application configuration object
# - custom application context object
# - custom request context object
pass
```
See more information in the [custom request context](./request.md#custom-request-context) section.
*Added in v23.6*

View File

@ -1,108 +0,0 @@
# Cookies
## Reading
.. column::
Cookies can be accessed via the `Request` objects `cookies` dictionary.
.. column::
```python
@app.route("/cookie")
async def test(request):
test_cookie = request.cookies.get("test")
return text(f"Test cookie: {test_cookie}")
```
.. tip:: FYI
💡 The `request.cookies` object is one of a few types that is a dictionary with each value being a `list`. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the `.get()` method to access the first element and not a `list`. If you do want a `list` of all items, you can use `.getlist()`.
*Added in v23.3*
## Writing
.. column::
When returning a response, cookies can be set on the `Response` object: `response.cookies`. This object is an instance of `CookieJar` which is a special sort of dictionary that automatically will write the response headers for you.
.. column::
```python
@app.route("/cookie")
async def test(request):
response = text("There's a cookie up in this response")
response.add_cookie(
"test",
"It worked!",
domain=".yummy-yummy-cookie.com",
httponly=True
)
return response
```
Response cookies can be set like dictionary values and have the following parameters available:
- `path: str` - The subset of URLs to which this cookie applies. Defaults to `/`.
- `domain: str` - Specifies the domain for which the cookie is valid. An explicitly specified domain must always start with a dot.
- `max_age: int` - Number of seconds the cookie should live for.
- `expires: datetime` - The time for the cookie to expire on the clients browser. Usually it is better to use max-age instead.
- `secure: bool` - Specifies whether the cookie will only be sent via HTTPS. Defaults to `True`.
- `httponly: bool` - Specifies whether the cookie cannot be read by JavaScript.
- `samesite: str` - Available values: Lax, Strict, and None. Defaults to `Lax`.
- `comment: str` - A comment (metadata).
- `host_prefix: bool` - Whether to add the `__Host-` prefix to the cookie.
- `secure_prefix: bool` - Whether to add the `__Secure-` prefix to the cookie.
- `partitioned: bool` - Whether to mark the cookie as partitioned.
To better understand the implications and usage of these values, it might be helpful to read the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) on [setting cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
.. tip:: FYI
By default, Sanic will set the `secure` flag to `True` to ensure that cookies are only sent over HTTPS as a sensible default. This should not be impactful for local development since secure cookies over HTTP should still be sent to `localhost`. For more information, you should read the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) on [secure cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Secure).
## Deleting
.. column::
Cookies can be removed semantically or explicitly.
.. column::
```python
@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
response.delete_cookie("eat_me")
# This cookie will self destruct in 5 seconds
response.add_cookie("fast_bake", "Be quick!", max_age=5)
return response
```
*Don't forget to add `path` or `domain` if needed!*
## Eating
.. column::
Sanic likes cookies
.. column::
.. attrs::
:class: is-size-1 has-text-centered
🍪

View File

@ -1,131 +0,0 @@
# Handlers
The next important building block are your _handlers_. These are also sometimes called "views".
In Sanic, a handler is any callable that takes at least a `Request` instance as an argument, and returns either an `HTTPResponse` instance, or a coroutine that does the same.
.. column::
Huh? 😕
It is a **function**; either synchronous or asynchronous.
The job of the handler is to respond to an endpoint and do something. This is where the majority of your business logic will go.
.. column::
```python
def i_am_a_handler(request):
return HTTPResponse()
async def i_am_ALSO_a_handler(request):
return HTTPResponse()
```
.. tip:: Heads up
If you want to learn more about encapsulating your logic, checkout [class based views](/guide/advanced/class-based-views.md).
.. column::
Then, all you need to do is wire it up to an endpoint. We'll learn more about [routing soon](./routing.md).
Let's look at a practical example.
- We use a convenience decorator on our app instance: `@app.get()`
- And a handy convenience method for generating out response object: `text()`
Mission accomplished :muscle:
.. column::
```python
from sanic.response import text
@app.get("/foo")
async def foo_handler(request):
return text("I said foo!")
```
---
## A word about _async_...
.. column::
It is entirely possible to write handlers that are synchronous.
In this example, we are using the _blocking_ `time.sleep()` to simulate 100ms of processing time. Perhaps this represents fetching data from a DB, or a 3rd-party website.
Using four (4) worker processes and a common benchmarking tool:
- **956** requests in 30.10s
- Or, about **31.76** requests/second
.. column::
```python
@app.get("/sync")
def sync_handler(request):
time.sleep(0.1)
return text("Done.")
```
.. column::
Just by changing to the asynchronous alternative `asyncio.sleep()`, we see an incredible change in performance. 🚀
Using the same four (4) worker processes:
- **115,590** requests in 30.08s
- Or, about **3,843.17** requests/second
.. attrs::
:class: is-size-3
🤯
Okay... this is a ridiculously overdramatic result. And any benchmark you see is inherently very biased. This example is meant to over-the-top show the benefit of `async/await` in the web world. Results will certainly vary. Tools like Sanic and other async Python libraries are not magic bullets that make things faster. They make them _more efficient_.
In our example, the asynchronous version is so much better because while one request is sleeping, it is able to start another one, and another one, and another one, and another one...
But, this is the point! Sanic is fast because it takes the available resources and squeezes performance out of them. It can handle many requests concurrently, which means more requests per second.
.. column::
```python
@app.get("/async")
async def async_handler(request):
await asyncio.sleep(0.1)
return text("Done.")
```
.. warning:: A common mistake!
Don't do this! You need to ping a website. What do you use? `pip install your-fav-request-library` 🙈
Instead, try using a client that is `async/await` capable. Your server will thank you. Avoid using blocking tools, and favor those that play well in the asynchronous ecosystem. If you need recommendations, check out [Awesome Sanic](https://github.com/mekicha/awesome-sanic).
Sanic uses [httpx](https://www.python-httpx.org/) inside of its testing package (sanic-testing) :wink:.
---
## A fully annotated handler
For those that are using type annotations...
```python
from sanic.response import HTTPResponse, text
from sanic.request import Request
@app.get("/typed")
async def typed_handler(request: Request) -> HTTPResponse:
return text("Done.")
```

View File

@ -1,229 +0,0 @@
# Headers
Request and response headers are available in the `Request` and `HTTPResponse` objects, respectively. They make use of the [`multidict` package](https://multidict.readthedocs.io/en/stable/multidict.html#cimultidict) that allows a single key to have multiple values.
.. tip:: FYI
Header keys are converted to *lowercase* when parsed. Capitalization is not considered for headers.
## Request
Sanic does attempt to do some normalization on request headers before presenting them to the developer, and also make some potentially meaningful extractions for common use cases.
.. column::
#### Tokens
Authorization tokens in the form `Token <token>` or `Bearer <token>` are extracted to the request object: `request.token`.
.. column::
```python
@app.route("/")
async def handler(request):
return text(request.token)
```
```bash
$ curl localhost:8000 \
-H "Authorization: Token ABCDEF12345679"
ABCDEF12345679
```
```bash
$ curl localhost:8000 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```
### Proxy headers
Sanic has special handling for proxy headers. See the [proxy headers](/guide/advanced/proxy-headers.md) section for more details.
### Host header and dynamic URL construction
.. column::
The *effective host* is available via `request.host`. This is not necessarily the same as the host header, as it prefers proxy-forwarded host and can be forced by the server name setting.
Webapps should generally use this accessor so that they can function the same no matter how they are deployed. The actual host header, if needed, can be found via `request.headers`
The effective host is also used in dynamic URL construction via `request.url_for`, which uses the request to determine the external address of a handler.
.. tip:: Be wary of malicious clients
These URLs can be manipulated by sending misleading host headers. `app.url_for` should be used instead if this is a concern.
.. column::
```python
app.config.SERVER_NAME = "https://example.com"
@app.route("/hosts", name="foo")
async def handler(request):
return json(
{
"effective host": request.host,
"host header": request.headers.get("host"),
"forwarded host": request.forwarded.get("host"),
"you are here": request.url_for("foo"),
}
)
```
```bash
$ curl localhost:8000/hosts
{
"effective host": "example.com",
"host header": "localhost:8000",
"forwarded host": null,
"you are here": "https://example.com/hosts"
}
```
### Other headers
.. column::
All request headers are available on `request.headers`, and can be accessed in dictionary form. Capitalization is not considered for headers, and can be accessed using either uppercase or lowercase keys.
.. column::
```python
@app.route("/")
async def handler(request):
return json(
{
"foo_weakref": request.headers["foo"],
"foo_get": request.headers.get("Foo"),
"foo_getone": request.headers.getone("FOO"),
"foo_getall": request.headers.getall("fOo"),
"all": list(request.headers.items()),
}
)
```
```bash
$ curl localhost:9999/headers -H "Foo: one" -H "FOO: two"|jq
{
"foo_weakref": "one",
"foo_get": "one",
"foo_getone": "one",
"foo_getall": [
"one",
"two"
],
"all": [
[
"host",
"localhost:9999"
],
[
"user-agent",
"curl/7.76.1"
],
[
"accept",
"*/*"
],
[
"foo",
"one"
],
[
"foo",
"two"
]
]
}
```
.. tip:: FYI
💡 The request.headers object is one of a few types that is a dictionary with each value being a list. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the .get() or .getone() methods to access the first element and not a list. If you do want a list of all items, you can use .getall().
### Request ID
.. column::
Often it is convenient or necessary to track a request by its `X-Request-ID` header. You can easily access that as: `request.id`.
.. column::
```python
@app.route("/")
async def handler(request):
return text(request.id)
```
```bash
$ curl localhost:8000 \
-H "X-Request-ID: ABCDEF12345679"
ABCDEF12345679
```
## Response
Sanic will automatically set the following response headers (when appropriate) for you:
- `content-length`
- `content-type`
- `connection`
- `transfer-encoding`
In most circumstances, you should never need to worry about setting these headers.
.. column::
Any other header that you would like to set can be done either in the route handler, or a response middleware.
.. column::
```python
@app.route("/")
async def handler(request):
return text("Done.", headers={"content-language": "en-US"})
@app.middleware("response")
async def add_csp(request, response):
response.headers["content-security-policy"] = "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';base-uri 'self';form-action 'self'"
```
.. column::
A common [middleware](middleware.md) you might want is to add a `X-Request-ID` header to every response. As stated above: `request.id` will provide the ID from the incoming request. But, even if no ID was supplied in the request headers, one will be automatically supplied for you.
[See API docs for more details](https://sanic.readthedocs.io/en/latest/sanic/api_reference.html#sanic.request.Request.id)
.. column::
```python
@app.route("/")
async def handler(request):
return text(str(request.id))
@app.on_response
async def add_request_id_header(request, response):
response.headers["X-Request-ID"] = request.id
```
```bash
$ curl localhost:8000 -i
HTTP/1.1 200 OK
X-Request-ID: 805a958e-9906-4e7a-8fe0-cbe83590431b
content-length: 36
connection: keep-alive
content-type: text/plain; charset=utf-8
805a958e-9906-4e7a-8fe0-cbe83590431b
```

View File

@ -1,243 +0,0 @@
# Listeners
Sanic provides you with eight (8) opportunities to inject an operation into the life cycle of your application server. This does not include the [signals](../advanced/signals.md), which allow further injection customization.
There are two (2) that run **only** on your main Sanic process (ie, once per call to `sanic server.app`.)
- `main_process_start`
- `main_process_stop`
There are also two (2) that run **only** in a reloader process if auto-reload has been turned on.
- `reload_process_start`
- `reload_process_stop`
*Added `reload_process_start` and `reload_process_stop` in v22.3*
There are four (4) that enable you to execute startup/teardown code as your server starts or closes.
- `before_server_start`
- `after_server_start`
- `before_server_stop`
- `after_server_stop`
The life cycle of a worker process looks like this:
.. mermaid::
sequenceDiagram
autonumber
participant Process
participant Worker
participant Listener
participant Handler
Note over Process: sanic server.app
loop
Process->>Listener: @app.main_process_start
Listener->>Handler: Invoke event handler
end
Process->>Worker: Run workers
loop Start each worker
loop
Worker->>Listener: @app.before_server_start
Listener->>Handler: Invoke event handler
end
Note over Worker: Server status: started
loop
Worker->>Listener: @app.after_server_start
Listener->>Handler: Invoke event handler
end
Note over Worker: Server status: ready
end
Process->>Worker: Graceful shutdown
loop Stop each worker
loop
Worker->>Listener: @app.before_server_stop
Listener->>Handler: Invoke event handler
end
Note over Worker: Server status: stopped
loop
Worker->>Listener: @app.after_server_stop
Listener->>Handler: Invoke event handler
end
Note over Worker: Server status: closed
end
loop
Process->>Listener: @app.main_process_stop
Listener->>Handler: Invoke event handler
end
Note over Process: exit
The reloader process live outside of this worker process inside of a process that is responsible for starting and stopping the Sanic processes. Consider the following example:
```python
@app.reload_process_start
async def reload_start(*_):
print(">>>>>> reload_start <<<<<<")
@app.main_process_start
async def main_start(*_):
print(">>>>>> main_start <<<<<<")
```
If this application were run with auto-reload turned on, the `reload_start` function would be called once. This is contrasted with `main_start`, which would be run every time a file is save and the reloader restarts the applicaition process.
## Attaching a listener
.. column::
The process to setup a function as a listener is similar to declaring a route.
The currently running `Sanic()` instance is injected into the listener.
.. column::
```python
async def setup_db(app):
app.ctx.db = await db_setup()
app.register_listener(setup_db, "before_server_start")
```
.. column::
The `Sanic` app instance also has a convenience decorator.
.. column::
```python
@app.listener("before_server_start")
async def setup_db(app):
app.ctx.db = await db_setup()
```
.. column::
Prior to v22.3, both the application instance and the current event loop were injected into the function. However, only the application instance is injected by default. If your function signature will accept both, then both the application and the loop will be injected as shown here.
.. column::
```python
@app.listener("before_server_start")
async def setup_db(app, loop):
app.ctx.db = await db_setup()
```
.. column::
You can shorten the decorator even further. This is helpful if you have an IDE with autocomplete.
.. column::
```python
@app.before_server_start
async def setup_db(app):
app.ctx.db = await db_setup()
```
## Order of execution
Listeners are executed in the order they are declared during startup, and reverse order of declaration during teardown
| | Phase | Order |
|-----------------------|-----------------|---------|
| `main_process_start` | main startup | regular 🙂 ⬇️ |
| `before_server_start` | worker startup | regular 🙂 ⬇️ |
| `after_server_start` | worker startup | regular 🙂 ⬇️ |
| `before_server_stop` | worker shutdown | 🙃 ⬆️ reverse |
| `after_server_stop` | worker shutdown | 🙃 ⬆️ reverse |
| `main_process_stop` | main shutdown | 🙃 ⬆️ reverse |
Given the following setup, we should expect to see this in the console if we run two workers.
.. column::
```python
@app.listener("before_server_start")
async def listener_1(app, loop):
print("listener_1")
@app.before_server_start
async def listener_2(app, loop):
print("listener_2")
@app.listener("after_server_start")
async def listener_3(app, loop):
print("listener_3")
@app.after_server_start
async def listener_4(app, loop):
print("listener_4")
@app.listener("before_server_stop")
async def listener_5(app, loop):
print("listener_5")
@app.before_server_stop
async def listener_6(app, loop):
print("listener_6")
@app.listener("after_server_stop")
async def listener_7(app, loop):
print("listener_7")
@app.after_server_stop
async def listener_8(app, loop):
print("listener_8")
```
.. column::
```bash
[pid: 1000000] [INFO] Goin' Fast @ http://127.0.0.1:9999
[pid: 1000000] [INFO] listener_0
[pid: 1111111] [INFO] listener_1
[pid: 1111111] [INFO] listener_2
[pid: 1111111] [INFO] listener_3
[pid: 1111111] [INFO] listener_4
[pid: 1111111] [INFO] Starting worker [1111111]
[pid: 1222222] [INFO] listener_1
[pid: 1222222] [INFO] listener_2
[pid: 1222222] [INFO] listener_3
[pid: 1222222] [INFO] listener_4
[pid: 1222222] [INFO] Starting worker [1222222]
[pid: 1111111] [INFO] Stopping worker [1111111]
[pid: 1222222] [INFO] Stopping worker [1222222]
[pid: 1222222] [INFO] listener_6
[pid: 1222222] [INFO] listener_5
[pid: 1222222] [INFO] listener_8
[pid: 1222222] [INFO] listener_7
[pid: 1111111] [INFO] listener_6
[pid: 1111111] [INFO] listener_5
[pid: 1111111] [INFO] listener_8
[pid: 1111111] [INFO] listener_7
[pid: 1000000] [INFO] listener_9
[pid: 1000000] [INFO] Server Stopped
```
In the above example, notice how there are three processes running:
- `pid: 1000000` - The *main* process
- `pid: 1111111` - Worker 1
- `pid: 1222222` - Worker 2
*Just because our example groups all of one worker and then all of another, in reality since these are running on separate processes, the ordering between processes is not guaranteed. But, you can be sure that a single worker will **always** maintain its order.*
.. tip:: FYI
The practical result of this is that if the first listener in `before_server_start` handler setups a database connection, listeners that are registered after it can rely upon that connection being alive both when they are started and stopped.
## ASGI Mode
If you are running your application with an ASGI server, then make note of the following changes:
- `reload_process_start` and `reload_process_stop` will be **ignored**
- `main_process_start` and `main_process_stop` will be **ignored**
- `before_server_start` will run as early as it can, and will be before `after_server_start`, but technically, the server is already running at that point
- `after_server_stop` will run as late as it can, and will be after `before_server_stop`, but technically, the server is still running at that point

View File

@ -1,229 +0,0 @@
# Middleware
Whereas listeners allow you to attach functionality to the lifecycle of a worker process, middleware allows you to attach functionality to the lifecycle of an HTTP stream.
You can execute middleware either _before_ the handler is executed, or _after_.
.. mermaid::
sequenceDiagram
autonumber
participant Worker
participant Middleware
participant MiddlewareHandler
participant RouteHandler
Note over Worker: Incoming HTTP request
loop
Worker->>Middleware: @app.on_request
Middleware->>MiddlewareHandler: Invoke middleware handler
MiddlewareHandler-->>Worker: Return response (optional)
end
rect rgba(255, 13, 104, .1)
Worker->>RouteHandler: Invoke route handler
RouteHandler->>Worker: Return response
end
loop
Worker->>Middleware: @app.on_response
Middleware->>MiddlewareHandler: Invoke middleware handler
MiddlewareHandler-->>Worker: Return response (optional)
end
Note over Worker: Deliver response
## Attaching middleware
.. column::
This should probably look familiar by now. All you need to do is declare when you would like the middleware to execute: on the `request` or on the `response`.
.. column::
```python
async def extract_user(request):
request.ctx.user = await extract_user_from_request(request)
app.register_middleware(extract_user, "request")
```
.. column::
Again, the `Sanic` app instance also has a convenience decorator.
.. column::
```python
@app.middleware("request")
async def extract_user(request):
request.ctx.user = await extract_user_from_request(request)
```
.. column::
Response middleware receives both the `request` and `response` arguments.
.. column::
```python
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
```
.. column::
You can shorten the decorator even further. This is helpful if you have an IDE with autocomplete.
This is the preferred usage, and is what we will use going forward.
.. column::
```python
@app.on_request
async def extract_user(request):
...
@app.on_response
async def prevent_xss(request, response):
...
```
## Modification
Middleware can modify the request or response parameter it is given, _as long as it does not return it_.
.. column::
#### Order of execution
1. Request middleware: `add_key`
2. Route handler: `index`
3. Response middleware: `prevent_xss`
4. Response middleware: `custom_banner`
.. column::
```python
@app.on_request
async def add_key(request):
# Arbitrary data may be stored in request context:
request.ctx.foo = "bar"
@app.on_response
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.on_response
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
@app.get("/")
async def index(request):
return text(request.ctx.foo)
```
.. column::
You can modify the `request.match_info`. A useful feature that could be used, for example, in middleware to convert `a-slug` to `a_slug`.
.. column::
```python
@app.on_request
def convert_slug_to_underscore(request: Request):
request.match_info["slug"] = request.match_info["slug"].replace("-", "_")
@app.get("/<slug:slug>")
async def handler(request, slug):
return text(slug)
```
```
$ curl localhost:9999/foo-bar-baz
foo_bar_baz
```
## Responding early
.. column::
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 route handler is reached, the handler will **not** be called. Returning a response will also prevent any further middleware from running.
.. tip::
You can return a `None` value to stop the execution of the middleware handler to allow the request to process as normal. This can be useful when using early return to avoid processing requests inside of that middleware handler.
.. column::
```python
@app.on_request
async def halt_request(request):
return text("I halted the request")
@app.on_response
async def halt_response(request, response):
return text("I halted the response")
```
## Order of execution
Request middleware is executed in the order declared. Response middleware is executed in **reverse order**.
Given the following setup, we should expect to see this in the console.
.. column::
```python
@app.on_request
async def middleware_1(request):
print("middleware_1")
@app.on_request
async def middleware_2(request):
print("middleware_2")
@app.on_response
async def middleware_3(request, response):
print("middleware_3")
@app.on_response
async def middleware_4(request, response):
print("middleware_4")
@app.get("/handler")
async def handler(request):
print("~ handler ~")
return text("Done.")
```
.. column::
```bash
middleware_1
middleware_2
~ handler ~
middleware_4
middleware_3
[INFO][127.0.0.1:44788]: GET http://localhost:8000/handler 200 5
```
### Middleware priority
.. column::
You can modify the order of execution of middleware by assigning it a higher priority. This happens inside of the middleware definition. The higher the value, the earlier it will execute relative to other middleware. The default priority for middleware is `0`.
.. column::
```python
@app.on_request
async def low_priority(request):
...
@app.on_request(priority=99)
async def high_priority(request):
...
```
*Added in v22.9*

View File

@ -1,327 +0,0 @@
# Request
The `Request` instance contains **a lot** of helpful information available on its parameters. Refer to the [API documentation](https://sanic.readthedocs.io/) for full details.
## Body
The `Request` object allows you to access the content of the request body in a few different ways.
### JSON
.. column::
**Parameter**: `request.json`
**Description**: The parsed JSON object
.. column::
```bash
$ curl localhost:8000 -d '{"foo": "bar"}'
```
```python
>>> print(request.json)
{'foo': 'bar'}
```
### Raw
.. column::
**Parameter**: `request.body`
**Description**: The raw bytes from the request body
.. column::
```bash
$ curl localhost:8000 -d '{"foo": "bar"}'
```
```python
>>> print(request.body)
b'{"foo": "bar"}'
```
### Form
.. column::
**Parameter**: `request.form`
**Description**: The form data
.. tip:: FYI
The `request.form` object is one of a few types that is a dictionary with each value being a list. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the `.get()` method to access the first element and not a list. If you do want a list of all items, you can use `.getlist()`.
.. column::
```bash
$ curl localhost:8000 -d 'foo=bar'
```
```python
>>> print(request.body)
b'foo=bar'
>>> print(request.form)
{'foo': ['bar']}
>>> print(request.form.get("foo"))
bar
>>> print(request.form.getlist("foo"))
['bar']
```
### Uploaded
.. column::
**Parameter**: `request.files`
**Description**: The files uploaded to the server
.. tip:: FYI
The `request.files` object is one of a few types that is a dictionary with each value being a list. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the `.get()` method to access the first element and not a list. If you do want a list of all items, you can use `.getlist()`.
.. column::
```bash
$ curl -F 'my_file=@/path/to/TEST' http://localhost:8000
```
```python
>>> print(request.body)
b'--------------------------cb566ad845ad02d3\r\nContent-Disposition: form-data; name="my_file"; filename="TEST"\r\nContent-Type: application/octet-stream\r\n\r\nhello\n\r\n--------------------------cb566ad845ad02d3--\r\n'
>>> print(request.files)
{'my_file': [File(type='application/octet-stream', body=b'hello\n', name='TEST')]}
>>> print(request.files.get("my_file"))
File(type='application/octet-stream', body=b'hello\n', name='TEST')
>>> print(request.files.getlist("my_file"))
[File(type='application/octet-stream', body=b'hello\n', name='TEST')]
```
## Context
### Request context
The `request.ctx` object is your playground to store whatever information you need to about the request.
This is often used to store items like authenticated user details. We will get more into [middleware](./middleware.md) later, but here is a simple example.
```python
@app.on_request
async def run_before_handler(request):
request.ctx.user = await fetch_user_by_token(request.token)
@app.route('/hi')
async def hi_my_name_is(request):
return text("Hi, my name is {}".format(request.ctx.user.name))
```
A typical use case would be to store the user object acquired from database in an authentication middleware. Keys added are accessible to all later middleware as well as the handler over the duration of the request.
Custom context is reserved for applications and extensions. Sanic itself makes no use of it.
### Connection context
.. column::
Often times your API will need to serve multiple concurrent (or consecutive) requests to the same client. This happens, for example, very often with progressive web apps that need to query multiple endpoints to get data.
The HTTP protocol calls for an easing of overhead time caused by the connection with the use of [keep alive headers](../deployment/configuration.md#keep-alive-timeout).
When multiple requests share a single connection, Sanic provides a context object to allow those requests to share state.
.. column::
```python
@app.on_request
async def increment_foo(request):
if not hasattr(request.conn_info.ctx, "foo"):
request.conn_info.ctx.foo = 0
request.conn_info.ctx.foo += 1
@app.get("/")
async def count_foo(request):
return text(f"request.conn_info.ctx.foo={request.conn_info.ctx.foo}")
```
```bash
$ curl localhost:8000 localhost:8000 localhost:8000
request.conn_info.ctx.foo=1
request.conn_info.ctx.foo=2
request.conn_info.ctx.foo=3
```
### Custom Request Objects
As dicussed in [application customization](./app.md#custom-requests), you can create a subclass of `sanic.Request` to add additional functionality to the request object. This is useful for adding additional attributes or methods that are specific to your application.
.. column::
For example, imagine your application sends a custom header that contains a user ID. You can create a custom request object that will parse that header and store the user ID for you.
.. column::
```python
from sanic import Sanic, Request
class CustomRequest(Request):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user_id = self.headers.get("X-User-ID")
app = Sanic("Example", request_class=CustomRequest)
```
.. column::
Now, in your handlers, you can access the `user_id` attribute.
.. column::
```python
@app.route("/")
async def handler(request: CustomRequest):
return text(f"User ID: {request.user_id}")
```
.. new:: NEW in v23.6
### Custom Request Context
By default, the request context (`request.ctx`) is a `SimpleNamespace` object allowing you to set arbitrary attributes on it. While this is super helpful to reuse logic across your application, it can be difficult in the development experience since the IDE will not know what attributes are available.
To help with this, you can create a custom request context object that will be used instead of the default `SimpleNamespace`. This allows you to add type hints to the context object and have them be available in your IDE.
.. column::
Start by subclassing the `sanic.Request` class to create a custom request type. Then, you will need to add a `make_context()` method that returns an instance of your custom context object. *NOTE: the `make_context` method should be a static method.*
.. column::
```python
from sanic import Sanic, Request
from types import SimpleNamespace
class CustomRequest(Request):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ctx.user_id = self.headers.get("X-User-ID")
@staticmethod
def make_context() -> CustomContext:
return CustomContext()
@dataclass
class CustomContext:
user_id: str = None
```
*Added in v23.6*
## Parameters
.. column::
Values that are extracted from the path are injected into the handler as parameters, or more specifically as keyword arguments. There is much more detail about this in the [Routing section](./routing.md).
.. column::
```python
@app.route('/tag/<tag>')
async def tag_handler(request, tag):
return text("Tag - {}".format(tag))
```
## Arguments
There are two attributes on the `request` instance to get query parameters:
- `request.args`
- `request.query_args`
```bash
$ curl http://localhost:8000\?key1\=val1\&key2\=val2\&key1\=val3
```
```python
>>> print(request.args)
{'key1': ['val1', 'val3'], 'key2': ['val2']}
>>> print(request.args.get("key1"))
val1
>>> print(request.args.getlist("key1"))
['val1', 'val3']
>>> print(request.query_args)
[('key1', 'val1'), ('key2', 'val2'), ('key1', 'val3')]
>>> print(request.query_string)
key1=val1&key2=val2&key1=val3
```
.. tip:: FYI
The `request.args` object is one of a few types that is a dictionary with each value being a list. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the `.get()` method to access the first element and not a list. If you do want a list of all items, you can use `.getlist()`.
## Current request getter
Sometimes you may find that you need access to the current request in your application in a location where it is not accessible. A typical example might be in a `logging` format. You can use `Request.get_current()` to fetch the current request (if any).
```python
import logging
from sanic import Request, Sanic, json
from sanic.exceptions import SanicException
from sanic.log import LOGGING_CONFIG_DEFAULTS
LOGGING_FORMAT = (
"%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
"%(request_id)s %(request)s %(message)s %(status)d %(byte)d"
)
old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
record.request_id = ""
try:
request = Request.get_current()
except SanicException:
...
else:
record.request_id = str(request.id)
return record
logging.setLogRecordFactory(record_factory)
LOGGING_CONFIG_DEFAULTS["formatters"]["access"]["format"] = LOGGING_FORMAT
app = Sanic("Example", log_config=LOGGING_CONFIG_DEFAULTS)
```
In this example, we are adding the `request.id` to every access log message.
*Added in v22.6*

View File

@ -1,265 +0,0 @@
# Response
All [handlers](./handlers.md) *usually* return a response object, and [middleware](./middleware.md) may optionally return a response object.
To clarify that statement:
- unless the handler is a streaming endpoint handling its own pattern for sending bytes to the client, the return value must be an instance of `sanic.HTTPResponse` (to learn more about this exception see [streaming responses](../advanced/streaming.md#response-streaming))
- if a middleware returns a response object, that will be used instead of whatever the handler would do (see [middleware](./middleware.md) to learn more)
A most basic handler would look like the following. The `HTTPResponse` object will allow you to set the status, body, and headers to be returned to the client.
```python
from sanic import HTTPResponse, Sanic
app = Sanic("TestApp")
@app.route("")
def handler(_):
return HTTPResponse()
```
However, usually it is easier to use one of the convenience methods discussed below.
## Methods
The easiest way to generate a response object is to use one of the nine (9) convenience methods.
### Text
.. column::
**Default Content-Type**: `text/plain; charset=utf-8`
**Description**: Returns plain text
.. column::
```python
from sanic import text
@app.route("/")
async def handler(request):
return text("Hi 😎")
```
### HTML
.. column::
**Default Content-Type**: `text/html; charset=utf-8`
**Description**: Returns an HTML document
.. column::
```python
from sanic import html
@app.route("/")
async def handler(request):
return html('<!DOCTYPE html><html lang="en"><meta charset="UTF-8"><div>Hi 😎</div>')
```
### JSON
.. column::
**Default Content-Type**: `application/json`
**Description**: Returns a JSON document
.. column::
```python
from sanic import json
@app.route("/")
async def handler(request):
return json({"foo": "bar"})
```
By default, Sanic ships with [`ujson`](https://github.com/ultrajson/ultrajson) as its JSON encoder of choice. It is super simple to change this if you want.
```python
from orjson import dumps
json({"foo": "bar"}, dumps=dumps)
```
If `ujson` is not installed, it will fall back to the standard library `json` module.
You may additionally declare which implementation to use globally across your application at initialization:
```python
from orjson import dumps
app = Sanic(..., dumps=dumps)
```
### File
.. column::
**Default Content-Type**: N/A
**Description**: Returns a file
.. column::
```python
from sanic import file
@app.route("/")
async def handler(request):
return await file("/path/to/whatever.png")
```
Sanic will examine the file, and try and guess its mime type and use an appropriate value for the content type. You could be explicit, if you would like:
```python
file("/path/to/whatever.png", mime_type="image/png")
```
You can also choose to override the file name:
```python
file("/path/to/whatever.png", filename="super-awesome-incredible.png")
```
### File Streaming
.. column::
**Default Content-Type**: N/A
**Description**: Streams a file to a client, useful when streaming large files, like a video
.. column::
```python
from sanic.response import file_stream
@app.route("/")
async def handler(request):
return await file_stream("/path/to/whatever.mp4")
```
Like the `file()` method, `file_stream()` will attempt to determine the mime type of the file.
### Raw
.. column::
**Default Content-Type**: `application/octet-stream`
**Description**: Send raw bytes without encoding the body
.. column::
```python
from sanic import raw
@app.route("/")
async def handler(request):
return raw(b"raw bytes")
```
### Redirect
.. column::
**Default Content-Type**: `text/html; charset=utf-8`
**Description**: Send a `302` response to redirect the client to a different path
.. column::
```python
from sanic import redirect
@app.route("/")
async def handler(request):
return redirect("/login")
```
### Empty
.. column::
**Default Content-Type**: N/A
**Description**: For responding with an empty message as defined by [RFC 2616](https://tools.ietf.org/search/rfc2616#section-7.2.1)
.. column::
```python
from sanic import empty
@app.route("/")
async def handler(request):
return empty()
```
Defaults to a `204` status.
## Default status
The default HTTP status code for the response is `200`. If you need to change it, it can be done by the response method.
```python
@app.post("/")
async def create_new(request):
new_thing = await do_create(request)
return json({"created": True, "id": new_thing.thing_id}, status=201)
```
## Returning JSON data
Starting in v22.12, When you use the `sanic.json` convenience method, it will return a subclass of `HTTPResponse` called `JSONResponse`. This object will
have several convenient methods available to modify common JSON body.
```python
from sanic import json
resp = json(...)
```
- `resp.set_body(<raw_body>)` - Set the body of the JSON object to the value passed
- `resp.append(<value>)` - Append a value to the body like `list.append` (only works if the root JSON is an array)
- `resp.extend(<value>)` - Extend a value to the body like `list.extend` (only works if the root JSON is an array)
- `resp.update(<value>)` - Update the body with a value like `dict.update` (only works if the root JSON is an object)
- `resp.pop()` - Pop a value like `list.pop` or `dict.pop` (only works if the root JSON is an array or an object)
.. warning::
The raw Python object is stored on the `JSONResponse` object as `raw_body`. While it is safe to overwrite this value with a new one, you should **not** attempt to mutate it. You should instead use the methods listed above.
```python
resp = json({"foo": "bar"})
# This is OKAY
resp.raw_body = {"foo": "bar", "something": "else"}
# This is better
resp.set_body({"foo": "bar", "something": "else"})
# This is also works well
resp.update({"something": "else"})
# This is NOT OKAY
resp.raw_body.update({"something": "else"})
```
```python
# Or, even treat it like a list
resp = json(["foo", "bar"])
# This is OKAY
resp.raw_body = ["foo", "bar", "something", "else"]
# This is better
resp.extend(["something", "else"])
# This is also works well
resp.append("something")
resp.append("else")
# This is NOT OKAY
resp.raw_body.append("something")
```
*Added in v22.9*

View File

@ -1,798 +0,0 @@
# Routing
.. column::
So far we have seen a lot of this decorator in different forms.
But what is it? And how do we use it?
.. column::
```python
@app.route("/stairway")
...
@app.get("/to")
...
@app.post("/heaven")
...
```
## Adding a route
.. column::
The most basic way to wire up a handler to an endpoint is with `app.add_route()`.
See [API docs](https://sanic.readthedocs.io/en/stable/sanic/api_reference.html#sanic.app.Sanic.url_for) for more details.
.. column::
```python
async def handler(request):
return text("OK")
app.add_route(handler, "/test")
```
.. column::
By default, routes are available as an HTTP `GET` call. You can change a handler to respond to one or more HTTP methods.
.. column::
```python
app.add_route(
handler,
'/test',
methods=["POST", "PUT"],
)
```
.. column::
Using the decorator syntax, the previous example is identical to this.
.. column::
```python
@app.route('/test', methods=["POST", "PUT"])
async def handler(request):
return text('OK')
```
## HTTP methods
Each of the standard HTTP methods has a convenience decorator.
### GET
```python
@app.get('/test')
async def handler(request):
return text('OK')
```
[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET)
### POST
```python
@app.post('/test')
async def handler(request):
return text('OK')
```
[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)
### PUT
```python
@app.put('/test')
async def handler(request):
return text('OK')
```
[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT)
### PATCH
```python
@app.patch('/test')
async def handler(request):
return text('OK')
```
[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH)
### DELETE
```python
@app.delete('/test')
async def handler(request):
return text('OK')
```
[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE)
### HEAD
```python
@app.head('/test')
async def handler(request):
return empty()
```
[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD)
### OPTIONS
```python
@app.options('/test')
async def handler(request):
return empty()
```
[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS)
.. warning::
By default, Sanic will **only** consume the incoming request body on non-safe HTTP methods (`POST`, `PUT`, `PATCH`, `DELETE`). If you want to receive data in the HTTP request on any other method, you will need to do one of the following two options:
**Option #1 - Tell Sanic to consume the body using `ignore_body`**
```python
@app.request("/path", ignore_body=False)
async def handler(_):
...
```
**Option #2 - Manually consume the body in the handler using `receive_body`**
```python
@app.get("/path")
async def handler(request: Request):
await request.receive_body()
```
## Path parameters
.. column::
Sanic allows for pattern matching, and for extracting values from URL paths. These parameters are then injected as keyword arguments in the route handler.
.. column::
```python
@app.get("/tag/<tag>")
async def tag_handler(request, tag):
return text("Tag - {}".format(tag))
```
.. column::
You can declare a type for the parameter. This will be enforced when matching, and also will type cast the variable.
.. column::
```python
@app.get("/foo/<foo_id:uuid>")
async def uuid_handler(request, foo_id: UUID):
return text("UUID - {}".format(foo_id))
```
### Supported types
### `str`
.. column::
**Regular expression applied**: `r"[^/]+"`
**Cast type**: `str`
**Example matches**:
- `/path/to/Bob`
- `/path/to/Python%203`
Beginning in v22.3 `str` will *not* match on empty strings. See `strorempty` for this behavior.
.. column::
```python
@app.route("/path/to/<foo:str>")
async def handler(request, foo: str):
...
```
### `strorempty`
.. column::
**Regular expression applied**: `r"[^/]*"`
**Cast type**: `str`
**Example matches**:
- `/path/to/Bob`
- `/path/to/Python%203`
- `/path/to/`
Unlike the `str` path parameter type, `strorempty` can also match on an empty string path segment.
*Added in v22.3*
.. column::
```python
@app.route("/path/to/<foo:strorempty>")
async def handler(request, foo: str):
...
```
### `int`
.. column::
**Regular expression applied**: `r"-?\d+"`
**Cast type**: `int`
**Example matches**:
- `/path/to/10`
- `/path/to/-10`
_Does not match float, hex, octal, etc_
.. column::
```python
@app.route("/path/to/<foo:int>")
async def handler(request, foo: int):
...
```
### `float`
.. column::
**Regular expression applied**: `r"-?(?:\d+(?:\.\d*)?|\.\d+)"`
**Cast type**: `float`
**Example matches**:
- `/path/to/10`
- `/path/to/-10`
- `/path/to/1.5`
.. column::
```python
@app.route("/path/to/<foo:float>")
async def handler(request, foo: float):
...
```
### `alpha`
.. column::
**Regular expression applied**: `r"[A-Za-z]+"`
**Cast type**: `str`
**Example matches**:
- `/path/to/Bob`
- `/path/to/Python`
_Does not match a digit, or a space or other special character_
.. column::
```python
@app.route("/path/to/<foo:alpha>")
async def handler(request, foo: str):
...
```
### `slug`
.. column::
**Regular expression applied**: `r"[a-z0-9]+(?:-[a-z0-9]+)*"`
**Cast type**: `str`
**Example matches**:
- `/path/to/some-news-story`
- `/path/to/or-has-digits-123`
*Added in v21.6*
.. column::
```python
@app.route("/path/to/<article:slug>")
async def handler(request, article: str):
...
```
### `path`
.. column::
**Regular expression applied**: `r"[^/].*?"`
**Cast type**: `str`
**Example matches**:
- `/path/to/hello`
- `/path/to/hello.txt`
- `/path/to/hello/world.txt`
.. column::
```python
@app.route("/path/to/<foo:path>")
async def handler(request, foo: str):
...
```
.. warning::
Because this will match on `/`, you should be careful and thoroughly test your patterns that use `path` so they do not capture traffic intended for another endpoint. Additionally, depending on how you use this type, you may be creating a path traversal vulnerability in your application. It is your job to protect your endpoint against this, but feel free to ask in our community channels for help if you need it :)
### `ymd`
.. column::
**Regular expression applied**: `r"^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))"`
**Cast type**: `datetime.date`
**Example matches**:
- `/path/to/2021-03-28`
.. column::
```python
@app.route("/path/to/<foo:ymd>")
async def handler(request, foo: datetime.date):
...
```
### `uuid`
.. column::
**Regular expression applied**: `r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}"`
**Cast type**: `UUID`
**Example matches**:
- `/path/to/123a123a-a12a-1a1a-a1a1-1a12a1a12345`
.. column::
```python
@app.route("/path/to/<foo:uuid>")
async def handler(request, foo: UUID):
...
```
### ext
.. column::
**Regular expression applied**: n/a
**Cast type**: *varies*
**Example matches**:
.. column::
```python
@app.route("/path/to/<foo:ext>")
async def handler(request, foo: str, ext: str):
...
```
| definition | example | filename | extension |
| --------------------------------- | ----------- | ----------- | ---------- |
| \<file:ext> | page.txt | `"page"` | `"txt"` |
| \<file:ext=jpg> | cat.jpg | `"cat"` | `"jpg"` |
| \<file:ext=jpg\|png\|gif\|svg> | cat.jpg | `"cat"` | `"jpg"` |
| <file=int:ext> | 123.txt | `123` | `"txt"` |
| <file=int:ext=jpg\|png\|gif\|svg> | 123.svg | `123` | `"svg"` |
| <file=float:ext=tar.gz> | 3.14.tar.gz | `3.14` | `"tar.gz"` |
File extensions can be matched using the special `ext` parameter type. It uses a special format that allows you to specify other types of parameter types as the file name, and one or more specific extensions as shown in the example table above.
It does *not* support the `path` parameter type.
*Added in v22.3*
### regex
.. column::
**Regular expression applied**: _whatever you insert_
**Cast type**: `str`
**Example matches**:
- `/path/to/2021-01-01`
This gives you the freedom to define specific matching patterns for your use case.
In the example shown, we are looking for a date that is in `YYYY-MM-DD` format.
.. column::
```python
@app.route(r"/path/to/<foo:([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))>")
async def handler(request, foo: str):
...
```
### Regex Matching
More often than not, compared with complex routing, the above example is too simple, and we use a completely different routing matching pattern, so here we will explain the advanced usage of regex matching in detail.
Sometimes, you want to match a part of a route:
```text
/image/123456789.jpg
```
If you wanted to match the file pattern, but only capture the numeric portion, you need to do some regex fun 😄:
```python
app.route(r"/image/<img_id:(?P<img_id>\d+)\.jpg>")
```
Further, these should all be acceptable:
```python
@app.get(r"/<foo:[a-z]{3}.txt>") # matching on the full pattern
@app.get(r"/<foo:([a-z]{3}).txt>") # defining a single matching group
@app.get(r"/<foo:(?P<foo>[a-z]{3}).txt>") # defining a single named matching group
@app.get(r"/<foo:(?P<foo>[a-z]{3}).(?:txt)>") # defining a single named matching group, with one or more non-matching groups
```
Also, if using a named matching group, it must be the same as the segment label.
```python
@app.get(r"/<foo:(?P<foo>\d+).jpg>") # OK
@app.get(r"/<foo:(?P<bar>\d+).jpg>") # NOT OK
```
For more regular usage methods, please refer to [Regular expression operations](https://docs.python.org/3/library/re.html)
## Generating a URL
.. column::
Sanic provides a method to generate URLs based on the handler method name: `app.url_for()`. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name.
.. column::
```python
@app.route('/')
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
# Redirect to `/posts/5`
return redirect(url)
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):
...
```
.. column::
You can pass any arbitrary number of keyword arguments. Anything that is _not_ a request parameter will be implemented as a part of the query string.
.. column::
```python
assert app.url_for(
"post_handler",
post_id=5,
arg_one="one",
arg_two="two",
) == "/posts/5?arg_one=one&arg_two=two"
```
.. column::
Also supported is passing multiple values for a single query key.
.. column::
```python
assert app.url_for(
"post_handler",
post_id=5,
arg_one=["one", "two"],
) == "/posts/5?arg_one=one&arg_one=two"
```
### Special keyword arguments
See [API Docs]() for more details.
```python
app.url_for("post_handler", post_id=5, arg_one="one", _anchor="anchor")
# '/posts/5?arg_one=one#anchor'
# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
app.url_for("post_handler", post_id=5, arg_one="one", _external=True)
# '//server/posts/5?arg_one=one'
# when specifying _scheme, _external must be True
app.url_for("post_handler", post_id=5, arg_one="one", _scheme="http", _external=True)
# 'http://server/posts/5?arg_one=one'
# you can pass all special arguments at once
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'
```
### Customizing a route name
.. column::
A custom route name can be used by passing a `name` argument while registering the route.
.. column::
```python
@app.get("/get", name="get_handler")
def handler(request):
return text("OK")
```
.. column::
Now, use this custom name to retrieve the URL
.. column::
```python
assert app.url_for("get_handler", foo="bar") == "/get?foo=bar"
```
## Websockets routes
.. column::
Websocket routing works similar to HTTP methods.
.. column::
```python
async def handler(request, ws):
message = "Start"
while True:
await ws.send(message)
message = await ws.recv()
app.add_websocket_route(handler, "/test")
```
.. column::
It also has a convenience decorator.
.. column::
```python
@app.websocket("/test")
async def handler(request, ws):
message = "Start"
while True:
await ws.send(message)
message = await ws.recv()
```
Read the [websockets section](/guide/advanced/websockets.md) to learn more about how they work.
## Strict slashes
.. column::
Sanic routes can be configured to strictly match on whether or not there is a trailing slash: `/`. This can be configured at a few levels and follows this order of precedence:
1. Route
2. Blueprint
3. BlueprintGroup
4. Application
.. column::
```python
# provide default strict_slashes value for all routes
app = Sanic(__file__, strict_slashes=True)
```
```python
# overwrite strict_slashes value for specific route
@app.get("/get", strict_slashes=False)
def handler(request):
return text("OK")
```
```python
# it also works for blueprints
bp = Blueprint(__file__, strict_slashes=True)
@bp.get("/bp/get", strict_slashes=False)
def handler(request):
return text("OK")
```
```python
bp1 = Blueprint(name="bp1", url_prefix="/bp1")
bp2 = Blueprint(
name="bp2",
url_prefix="/bp2",
strict_slashes=False,
)
# This will enforce strict slashes check on the routes
# under bp1 but ignore bp2 as that has an explicitly
# set the strict slashes check to false
group = Blueprint.group([bp1, bp2], strict_slashes=True)
```
## Static files
.. column::
In order to serve static files from Sanic, use `app.static()`.
The order of arguments is important:
1. Route the files will be served from
2. Path to the files on the server
See [API docs](https://sanic.readthedocs.io/en/stable/sanic/api/app.html#sanic.app.Sanic.static) for more details.
.. column::
```python
app.static("/static/", "/path/to/directory/")
```
.. tip::
It is generally best practice to end your directory paths with a trailing slash (`/this/is/a/directory/`). This removes ambiguity by being more explicit.
.. column::
You can also serve individual files.
.. column::
```python
app.static("/", "/path/to/index.html")
```
.. column::
It is also sometimes helpful to name your endpoint
.. column::
```python
app.static(
"/user/uploads/",
"/path/to/uploads/",
name="uploads",
)
```
.. column::
Retrieving the URLs works similar to handlers. But, we can also add the `filename` argument when we need a specific file inside a directory.
.. column::
```python
assert app.url_for(
"static",
name="static",
filename="file.txt",
) == "/static/file.txt"
```
```python
assert app.url_for(
"static",
name="uploads",
filename="image.png",
) == "/user/uploads/image.png"
```
.. tip::
If you are going to have multiple `static()` routes, then it is *highly* suggested that you manually name them. This will almost certainly alleviate potential hard to discover bugs.
```python
app.static("/user/uploads/", "/path/to/uploads/", name="uploads")
app.static("/user/profile/", "/path/to/profile/", name="profile_pics")
```
#### Auto index serving
.. column::
If you have a directory of static files that should be served by an index page, you can provide the filename of the index. Now, when reaching that directory URL, the index page will be served.
.. column::
```python
app.static("/foo/", "/path/to/foo/", index="index.html")
```
*Added in v23.3*
#### File browser
.. column::
When serving a directory from a static handler, Sanic can be configured to show a basic file browser instead using `directory_view=True`.
.. column::
```python
app.static("/uploads/", "/path/to/dir", directory_view=True)
```
You now have a browsable directory in your web browser:
![image](/assets/images/directory-view.png)
*Added in v23.3*
## Route context
.. column::
When a route is defined, you can add any number of keyword arguments with a `ctx_` prefix. These values will be injected into the route `ctx` object.
.. column::
```python
@app.get("/1", ctx_label="something")
async def handler1(request):
...
@app.get("/2", ctx_label="something")
async def handler2(request):
...
@app.get("/99")
async def handler99(request):
...
@app.on_request
async def do_something(request):
if request.route.ctx.label == "something":
...
```
*Added in v21.12*

View File

@ -1,135 +0,0 @@
# Background tasks
## Creating Tasks
It is often desirable and very convenient to make usage of [tasks](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) in async Python. Sanic provides a convenient method to add tasks to the currently **running** loop. It is somewhat similar to `asyncio.create_task`. For adding tasks before the 'App' loop is running, see next section.
```python
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())
```
.. column::
Sanic will attempt to automatically inject the app, passing it as an argument to the task.
.. column::
```python
async def auto_inject(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(auto_inject)
```
.. column::
Or you can pass the `app` argument explicitly.
.. column::
```python
async def explicit_inject(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(explicit_inject(app))
```
## Adding tasks before `app.run`
It is possible to add background tasks before the App is run ie. before `app.run`. To add a task before the App is run, it is recommended to not pass the coroutine object (ie. one created by calling the `async` callable), but instead just pass the callable and Sanic will create the coroutine object on **each worker**. Note: the tasks that are added such are run as `before_server_start` jobs and thus run on every worker (and not in the main process). This has certain consequences, please read [this comment](https://github.com/sanic-org/sanic/issues/2139#issuecomment-868993668) on [this issue](https://github.com/sanic-org/sanic/issues/2139) for further details.
To add work on the main process, consider adding work to [`@app.main_process_start`](./listeners.md). Note: the workers won't start until this work is completed.
.. column::
Example to add a task before `app.run`
.. column::
```python
async def slow_work():
...
async def even_slower(num):
...
app = Sanic(...)
app.add_task(slow_work) # Note: we are passing the callable and not coroutine object ...
app.add_task(even_slower(10)) # ... or we can call the function and pass the coroutine.
app.run(...)
```
## Named tasks
.. column::
When creating a task, you can ask Sanic to keep track of it for you by providing a `name`.
.. column::
```python
app.add_task(slow_work, name="slow_task")
```
.. column::
You can now retrieve that task instance from anywhere in your application using `get_task`.
.. column::
```python
task = app.get_task("slow_task")
```
.. column::
If that task needs to be cancelled, you can do that with `cancel_task`. Make sure that you `await` it.
.. column::
```python
await app.cancel_task("slow_task")
```
.. column::
All registered tasks can be found in the `app.tasks` property. To prevent cancelled tasks from filling up, you may want to run `app.purge_tasks` that will clear out any completed or cancelled tasks.
.. column::
```python
app.purge_tasks()
```
This pattern can be particularly useful with `websockets`:
```python
async def receiver(ws):
while True:
message = await ws.recv()
if not message:
break
print(f"Received: {message}")
@app.websocket("/feed")
async def feed(request, ws):
task_name = f"receiver:{request.id}"
request.app.add_task(receiver(ws), name=task_name)
try:
while True:
await request.app.event("my.custom.event")
await ws.send("A message")
finally:
# When the websocket closes, let's cleanup the task
await request.app.cancel_task(task_name)
request.app.purge_tasks()
```
*Added in v21.12*

View File

@ -1,453 +0,0 @@
# Blueprints
## Overview
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.
## Creating and registering
.. column::
First, you must create a blueprint. It has a very similar API as the `Sanic()` app instance with many of the same decorators.
.. column::
```python
# ./my_blueprint.py
from sanic.response import json
from sanic import Blueprint
bp = Blueprint("my_blueprint")
@bp.route("/")
async def bp_root(request):
return json({"my": "blueprint"})
```
.. column::
Next, you register it with the app instance.
.. column::
```python
from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.blueprint(bp)
```
Blueprints also have the same `websocket()` decorator and `add_websocket_route` method for implementing websockets.
.. column::
Beginning in v21.12, a Blueprint may be registered before or after adding objects to it. Previously, only objects attached to the Blueprint at the time of registration would be loaded into application instance.
.. column::
```python
app.blueprint(bp)
@bp.route("/")
async def bp_root(request):
...
```
## Copying
.. column::
Blueprints along with everything that is attached to them can be copied to new instances using the `copy()` method. The only required argument is to pass it a new `name`. However, you could also use this to override any of the values from the old blueprint.
.. column::
```python
v1 = Blueprint("Version1", version=1)
@v1.route("/something")
def something(request):
pass
v2 = v1.copy("Version2", version=2)
app.blueprint(v1)
app.blueprint(v2)
```
```
Available routes:
/v1/something
/v2/something
```
*Added in v21.9*
## Blueprint groups
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 whats seen from the front end. Consider this (quite contrived) example:
```text
api/
├──content/
│ ├──authors.py
│ ├──static.py
│ └──__init__.py
├──info.py
└──__init__.py
app.py
```
.. column::
#### First blueprint
.. column::
```python
# api/content/authors.py
from sanic import Blueprint
authors = Blueprint("content_authors", url_prefix="/authors")
```
.. column::
#### Second blueprint
.. column::
```python
# api/content/static.py
from sanic import Blueprint
static = Blueprint("content_static", url_prefix="/static")
```
.. column::
#### Blueprint group
.. column::
```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")
```
.. column::
#### Third blueprint
.. column::
```python
# api/info.py
from sanic import Blueprint
info = Blueprint("info", url_prefix="/info")
```
.. column::
#### Another blueprint group
.. column::
```python
# api/__init__.py
from sanic import Blueprint
from .content import content
from .info import info
api = Blueprint.group(content, info, url_prefix="/api")
```
.. column::
#### Main server
All blueprints are now registered
.. column::
```python
# app.py
from sanic import Sanic
from .api import api
app = Sanic(__name__)
app.blueprint(api)
```
### Blueprint group prefixes and composability
As shown in the code above, when you create a group of blueprints you can extend the URL prefix of all the blueprints in the group by passing the `url_prefix` argument to the `Blueprint.group` method. This is useful for creating a mock directory structure for your API.
.. new:: NEW in v23.6
In addition, there is a `name_prefix` argument that can be used to make blueprints reusable and composable. The is specifically necessary when applying a single blueprint to multiple groups. By doing this, the blueprint will be registered with a unique name for each group, which allows the blueprint to be registered multiple times and have its routes each properly named with a unique identifier.
.. column::
Consider this example. The routes built will be named as follows:
- `TestApp.group-a_bp1.route1`
- `TestApp.group-a_bp2.route2`
- `TestApp.group-b_bp1.route1`
- `TestApp.group-b_bp2.route2`
.. column::
```python
bp1 = Blueprint("bp1", url_prefix="/bp1")
bp2 = Blueprint("bp2", url_prefix="/bp2")
bp1.add_route(lambda _: ..., "/", name="route1")
bp2.add_route(lambda _: ..., "/", name="route2")
group_a = Blueprint.group(
bp1, bp2, url_prefix="/group-a", name_prefix="group-a"
)
group_b = Blueprint.group(
bp1, bp2, url_prefix="/group-b", name_prefix="group-b"
)
app = Sanic("TestApp")
app.blueprint(group_a)
app.blueprint(group_b)
```
*Name prefixing added in v23.6*
## Middleware
.. column::
Blueprints can also have middleware that is specifically registered for its endpoints only.
.. column::
```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")
```
.. column::
Similarly, using blueprint groups, it is possible to apply middleware to an entire group of nested blueprints.
.. column::
```python
bp1 = Blueprint("bp1", url_prefix="/bp1")
bp2 = Blueprint("bp2", url_prefix="/bp2")
@bp1.middleware("request")
async def bp1_only_middleware(request):
print("applied on Blueprint : bp1 Only")
@bp1.route("/")
async def bp1_route(request):
return text("bp1")
@bp2.route("/<param>")
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware("request")
async def group_middleware(request):
print("common middleware applied for both bp1 and bp2")
# Register Blueprint group under the app
app.blueprint(group)
```
## Exceptions
.. column::
Just like other [exception handling](./exceptions.md), you can define blueprint specific handlers.
.. column::
```python
@bp.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
## Static files
.. column::
Blueprints can also have their own static handlers
.. column::
```python
bp = Blueprint("bp", url_prefix="/bp")
bp.static("/web/path", "/folder/to/serve")
bp.static("/web/path", "/folder/to/server", name="uploads")
```
.. column::
Which can then be retrieved using `url_for()`. See [routing](/guide/basics/routing.md) for more information.
.. column::
```python
>>> print(app.url_for("static", name="bp.uploads", filename="file.txt"))
'/bp/web/path/file.txt'
```
## Listeners
.. column::
Blueprints can also implement [listeners](/guide/basics/listeners.md).
.. column::
```python
@bp.listener("before_server_start")
async def before_server_start(app, loop):
...
@bp.listener("after_server_stop")
async def after_server_stop(app, loop):
...
```
## Versioning
As discussed in the [versioning section](/guide/advanced/versioning.md), blueprints can be used to implement different versions of a web API.
.. column::
The `version` will be prepended to the routes as `/v1` or `/v2`, etc.
.. column::
```python
auth1 = Blueprint("auth", url_prefix="/auth", version=1)
auth2 = Blueprint("auth", url_prefix="/auth", version=2)
```
.. column::
When we register our blueprints on the app, the routes `/v1/auth` and `/v2/auth` will now point to the individual blueprints, which allows the creation of sub-sites for each API version.
.. column::
```python
from auth_blueprints import auth1, auth2
app = Sanic(__name__)
app.blueprint(auth1)
app.blueprint(auth2)
```
.. column::
It is also possible to group the blueprints under a `BlueprintGroup` entity and version multiple of them together at the
same time.
.. column::
```python
auth = Blueprint("auth", url_prefix="/auth")
metrics = Blueprint("metrics", url_prefix="/metrics")
group = Blueprint.group(auth, metrics, version="v1")
# This will provide APIs prefixed with the following URL path
# /v1/auth/ and /v1/metrics
```
## Composable
A `Blueprint` may be registered to multiple groups, and each of `BlueprintGroup` itself could be registered and nested further. This creates a limitless possibility `Blueprint` composition.
*Added in v21.6*
.. column::
Take a look at this example and see how the two handlers are actually mounted as five (5) distinct routes.
.. column::
```python
app = Sanic(__name__)
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
group = Blueprint.group(
blueprint_1,
blueprint_2,
version=1,
version_prefix="/api/v",
url_prefix="/grouped",
strict_slashes=True,
)
primary = Blueprint.group(group, url_prefix="/primary")
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
app.blueprint(group)
app.blueprint(primary)
app.blueprint(blueprint_1)
# The mounted paths:
# /api/v1/grouped/bp1/
# /api/v1/grouped/bp2/
# /api/v1/primary/grouped/bp1
# /api/v1/primary/grouped/bp2
# /bp1
```
## Generating a URL
When generating a url with `url_for()`, the endpoint name will be in the form:
```text
{blueprint_name}.{handler_name}
```

View File

@ -1,176 +0,0 @@
# Decorators
One of the best ways to create a consistent and DRY web API is to make use of decorators to remove functionality from the handlers, and make it repeatable across your views.
.. column::
Therefore, it is very common to see a Sanic view handler with several decorators on it.
.. column::
```python
@app.get("/orders")
@authorized("view_order")
@validate_list_params()
@inject_user()
async def get_order_details(request, params, user):
...
```
## Example
Here is a starter template to help you create decorators.
In this example, lets 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 = await 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"})
```
## Templates
Decorators are **fundamental** to building applications with Sanic. They increase the portability and maintainablity of your code.
In paraphrasing the Zen of Python: "[decorators] are one honking great idea -- let's do more of those!"
To make it easier to implement them, here are three examples of copy/pastable code to get you started.
.. column::
Don't forget to add these import statements. Although it is *not* necessary, using `@wraps` helps keep some of the metadata of your function intact. [See docs](https://docs.python.org/3/library/functools.html#functools.wraps). Also, we use the `isawaitable` pattern here to allow the route handlers to by regular or asynchronous functions.
.. column::
```python
from inspect import isawaitable
from functools import wraps
```
### With args
.. column::
Often, you will want a decorator that will *always* need arguments. Therefore, when it is implemented you will always be calling it.
```python
@app.get("/")
@foobar(1, 2)
async def handler(request: Request):
return text("hi")
```
.. column::
```python
def foobar(arg1, arg2):
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
response = f(request, *args, **kwargs)
if isawaitable(response):
response = await response
return response
return decorated_function
return decorator
```
### Without args
.. column::
Sometimes you want a decorator that will not take arguments. When this is the case, it is a nice convenience not to have to call it
```python
@app.get("/")
@foobar
async def handler(request: Request):
return text("hi")
```
.. column::
```python
def foobar(func):
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
response = f(request, *args, **kwargs)
if isawaitable(response):
response = await response
return response
return decorated_function
return decorator(func)
```
### With or Without args
.. column::
If you want a decorator with the ability to be called or not, you can follow this pattern. Using keyword only arguments is not necessary, but might make implementation simpler.
```python
@app.get("/")
@foobar(arg1=1, arg2=2)
async def handler(request: Request):
return text("hi")
```
```python
@app.get("/")
@foobar
async def handler(request: Request):
return text("hi")
```
.. column::
```python
def foobar(maybe_func=None, *, arg1=None, arg2=None):
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
response = f(request, *args, **kwargs)
if isawaitable(response):
response = await response
return response
return decorated_function
return decorator(maybe_func) if maybe_func else decorator
```

View File

@ -1,580 +0,0 @@
# Exceptions
## Using Sanic exceptions
Sometimes you just need to tell Sanic to halt execution of a handler and send back a status code response. You can raise a `SanicException` for this and Sanic will do the rest for you.
You can pass an optional `status_code` argument. By default, a SanicException will return an internal server error 500 response.
```python
from sanic.exceptions import SanicException
@app.route("/youshallnotpass")
async def no_no(request):
raise SanicException("Something went wrong.", status_code=501)
```
Sanic provides a number of standard exceptions. They each automatically will raise the appropriate HTTP status code in your response. [Check the API reference](https://sanic.readthedocs.io/en/latest/sanic/api_reference.html#module-sanic.exceptions) for more details.
.. column::
The more common exceptions you _should_ implement yourself include:
- `BadRequest` (400)
- `Unauthorized` (401)
- `Forbidden` (403)
- `NotFound` (404)
- `ServerError` (500)
.. column::
```python
from sanic import exceptions
@app.route("/login")
async def login(request):
user = await some_login_func(request)
if not user:
raise exceptions.NotFound(
f"Could not find user with username={request.json.username}"
)
```
## Exception properties
All exceptions in Sanic derive from `SanicException`. That class has a few properties on it that assist the developer in consistently reporting their exceptions across an application.
- `message`
- `status_code`
- `quiet`
- `headers`
- `context`
- `extra`
All of these properties can be passed to the exception when it is created, but the first three can also be used as class variables as we will see.
.. column::
### `message`
The `message` property obviously controls the message that will be displayed as with any other exception in Python. What is particularly useful is that you can set the `message` property on the class definition allowing for easy standardization of language across an application
.. column::
```python
class CustomError(SanicException):
message = "Something bad happened"
raise CustomError
# or
raise CustomError("Override the default message with something else")
```
.. column::
### `status_code`
This property is used to set the response code when the exception is raised. This can particularly be useful when creating custom 400 series exceptions that are usually in response to bad information coming from the client.
.. column::
```python
class TeapotError(SanicException):
status_code = 418
message = "Sorry, I cannot brew coffee"
raise TeapotError
# or
raise TeapotError(status_code=400)
```
.. column::
### `quiet`
By default, exceptions will be output by Sanic to the `error_logger`. Sometimes this may not be desirable, especially if you are using exceptions to trigger events in exception handlers (see [the following section](./exceptions.md#handling)). You can suppress the log output using `quiet=True`.
.. column::
```python
class SilentError(SanicException):
message = "Something happened, but not shown in logs"
quiet = True
raise SilentError
# or
raise InvalidUsage("blah blah", quiet=True)
```
.. column::
Sometimes while debugging you may want to globally ignore the `quiet=True` property. You can force Sanic to log out all exceptions regardless of this property using `NOISY_EXCEPTIONS`
*Added in v21.12*
.. column::
```python
app.config.NOISY_EXCEPTIONS = True
```
.. column::
### `headers`
Using `SanicException` as a tool for creating responses is super powerful. This is in part because not only can you control the `status_code`, but you can also control reponse headers directly from the exception.
.. column::
```python
class MyException(SanicException):
headers = {
"X-Foo": "bar"
}
raise MyException
# or
raise InvalidUsage("blah blah", headers={
"X-Foo": "bar"
})
```
.. column::
### `extra`
See [contextual exceptions](./exceptions.md#contextual-exceptions)
*Added in v21.12*
.. column::
```python
raise SanicException(..., extra={"name": "Adam"})
```
.. column::
### `context`
See [contextual exceptions](./exceptions.md#contextual-exceptions)
*Added in v21.12*
.. column::
```python
raise SanicException(..., context={"foo": "bar"})
```
## Handling
Sanic handles exceptions automatically by rendering an error page, so in many cases you don't need to handle them yourself. However, if you would like more control on what to do when an exception is raised, you can implement a handler yourself.
Sanic provides a decorator for this, which applies to not only the Sanic standard exceptions, but **any** exception that your application might throw.
.. column::
The easiest method to add a handler is to use `@app.exception()` and pass it one or more exceptions.
.. column::
```python
from sanic.exceptions import NotFound
@app.exception(NotFound, SomeCustomException)
async def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
.. column::
You can also create a catchall handler by catching `Exception`.
.. column::
```python
@app.exception(Exception)
async def catch_anything(request, exception):
...
```
.. column::
You can also use `app.error_handler.add()` to add error handlers.
.. column::
```python
async def server_error_handler(request, exception):
return text("Oops, server error", status=500)
app.error_handler.add(Exception, server_error_handler)
```
## Built-in error handling
Sanic ships with three formats for exceptions: HTML, JSON, and text. You can see examples of them below in the [Fallback handler](#fallback-handler) section.
.. column::
You can control _per route_ which format to use with the `error_format` keyword argument.
*Added in v21.9*
.. column::
```python
@app.request("/", error_format="text")
async def handler(request):
...
```
## Custom error handling
In some cases, you might want to add some more error handling functionality to what is provided by default. In that case, you can subclass Sanic's default error handler as such:
```python
from sanic.handlers import ErrorHandler
class CustomErrorHandler(ErrorHandler):
def default(self, request: Request, exception: Exception) -> HTTPResponse:
''' handles errors that have no error handlers assigned '''
# You custom error handling logic...
status_code = getattr(exception, "status_code", 500)
return json({
"error": str(exception),
"foo": "bar"
}, status=status_code)
app.error_handler = CustomErrorHandler()
```
## Fallback handler
Sanic comes with three fallback exception handlers:
1. HTML
2. Text
3. JSON
These handlers present differing levels of detail depending upon whether your application is in [debug mode](/guide/deployment/development.md) or not.
By default, Sanic will be in "auto" mode, which means that it will using the incoming request and potential matching handler to choose the appropriate response format. For example, when in a browser it should always provide an HTML error page. When using curl, you might see JSON or plain text.
### HTML
```python
app.config.FALLBACK_ERROR_FORMAT = "html"
```
.. column::
```python
app.config.DEBUG = True
```
![Error](/assets/images/error-display-html-debug.png)
.. column::
```python
app.config.DEBUG = False
```
![Error](/assets/images/error-display-html-prod.png)
### Text
```python
app.config.FALLBACK_ERROR_FORMAT = "text"
```
.. column::
```python
app.config.DEBUG = True
```
```sh
curl localhost:8000/exc -i
HTTP/1.1 500 Internal Server Error
content-length: 620
connection: keep-alive
content-type: text/plain; charset=utf-8
⚠️ 500 — Internal Server Error
==============================
That time when that thing broke that other thing? That happened.
ServerError: That time when that thing broke that other thing? That happened. while handling path /exc
Traceback of TestApp (most recent call last):
ServerError: That time when that thing broke that other thing? That happened.
File /path/to/sanic/app.py, line 979, in handle_request
response = await response
File /path/to/server.py, line 16, in handler
do_something(cause_error=True)
File /path/to/something.py, line 9, in do_something
raise ServerError(
```
.. column::
```python
app.config.DEBUG = False
```
```sh
curl localhost:8000/exc -i
HTTP/1.1 500 Internal Server Error
content-length: 134
connection: keep-alive
content-type: text/plain; charset=utf-8
⚠️ 500 — Internal Server Error
==============================
That time when that thing broke that other thing? That happened.
```
### JSON
```python
app.config.FALLBACK_ERROR_FORMAT = "json"
```
.. column::
```python
app.config.DEBUG = True
```
```sh
curl localhost:8000/exc -i
HTTP/1.1 500 Internal Server Error
content-length: 572
connection: keep-alive
content-type: application/jso
{
"description": "Internal Server Error",
"status": 500,
"message": "That time when that thing broke that other thing? That happened.",
"path": "/exc",
"args": {},
"exceptions": [
{
"type": "ServerError",
"exception": "That time when that thing broke that other thing? That happened.",
"frames": [
{
"file": "/path/to/sanic/app.py",
"line": 979,
"name": "handle_request",
"src": "response = await response"
},
{
"file": "/path/to/server.py",
"line": 16,
"name": "handler",
"src": "do_something(cause_error=True)"
},
{
"file": "/path/to/something.py",
"line": 9,
"name": "do_something",
"src": "raise ServerError("
}
]
}
]
}
```
.. column::
```python
app.config.DEBUG = False
```
```sh
curl localhost:8000/exc -i
HTTP/1.1 500 Internal Server Error
content-length: 129
connection: keep-alive
content-type: application/json
{
"description": "Internal Server Error",
"status": 500,
"message": "That time when that thing broke that other thing? That happened."
}
```
### Auto
Sanic also provides an option for guessing which fallback option to use.
```python
app.config.FALLBACK_ERROR_FORMAT = "auto"
```
## Contextual Exceptions
Default exception messages that simplify the ability to consistently raise exceptions throughout your application.
```python
class TeapotError(SanicException):
status_code = 418
message = "Sorry, I cannot brew coffee"
raise TeapotError
```
But this lacks two things:
1. A dynamic and predictable message format
2. The ability to add additional context to an error message (more on this in a moment)
*Added in v21.12*
Using one of Sanic's exceptions, you have two options to provide additional details at runtime:
```python
raise TeapotError(extra={"foo": "bar"}, context={"foo": "bar"})
```
What's the difference and when should you decide to use each?
- `extra` - The object itself will **never** be sent to a production client. It is meant for internal use only. What could it be used for?
- Generating (as we will see in a minute) a dynamic error message
- Providing runtime details to a logger
- Debug information (when in development mode, it is rendered)
- `context` - This object is **always** sent to production clients. It is generally meant to be used to send additional details about the context of what happened. What could it be used for?
- Providing alternative values on a `BadRequest` validation issue
- Responding with helpful details for your customers to open a support ticket
- Displaying state information like current logged in user info
### Dynamic and predictable message using `extra`
Sanic exceptions can be raised using `extra` keyword arguments to provide additional information to a raised exception instance.
```python
class TeapotError(SanicException):
status_code = 418
@property
def message(self):
return f"Sorry {self.extra['name']}, I cannot make you coffee"
raise TeapotError(extra={"name": "Adam"})
```
The new feature allows the passing of `extra` meta to the exception instance, which can be particularly useful as in the above example to pass dynamic data into the message text. This `extra` info object **will be suppressed** when in `PRODUCTION` mode, but displayed in `DEVELOPMENT` mode.
.. column::
**DEVELOPMENT**
![image](~@assets/images/error-extra-debug.png)
.. column::
**PRODUCTION**
![image](~@assets/images/error-extra-prod.png)
### Additional `context` to an error message
Sanic exceptions can also be raised with a `context` argument to pass intended information along to the user about what happened. This is particularly useful when creating microservices or an API intended to pass error messages in JSON format. In this use case, we want to have some context around the exception beyond just a parseable error message to return details to the client.
```python
raise TeapotError(context={"foo": "bar"})
```
This is information **that we want** to always be passed in the error (when it is available). Here is what it should look like:
.. column::
**PRODUCTION**
```json
{
"description": "I'm a teapot",
"status": 418,
"message": "Sorry Adam, I cannot make you coffee",
"context": {
"foo": "bar"
}
}
```
.. column::
**DEVELOPMENT**
```json
{
"description": "I'm a teapot",
"status": 418,
"message": "Sorry Adam, I cannot make you coffee",
"context": {
"foo": "bar"
},
"path": "/",
"args": {},
"exceptions": [
{
"type": "TeapotError",
"exception": "Sorry Adam, I cannot make you coffee",
"frames": [
{
"file": "handle_request",
"line": 83,
"name": "handle_request",
"src": ""
},
{
"file": "/tmp/p.py",
"line": 17,
"name": "handler",
"src": "raise TeapotError("
}
]
}
]
}
```
.. new:: NEW in v23.6
## Error reporting
Sanic has a [signal](../advanced/signals.md#built-in-signals) that allows you to hook into the exception reporting process. This is useful if you want to send exception information to a third party service like Sentry or Rollbar. This can be conveniently accomplished by attaching an error reporting handler as show below:
```python
@app.report_exception
async def catch_any_exception(app: Sanic, exception: Exception):
print("Caught exception:", exception)
```
.. note::
This handler will be dispatched into a background task and **IS NOT** intended for use to manipulate any response data. It is intended to be used for logging or reporting purposes only, and should not impact the ability of your application to return the error response to the client.
*Added in v23.6*

View File

@ -1,97 +0,0 @@
# Logging
Sanic allows you to do different types of logging (access log, error log) on the requests based on the [Python logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on Python logging if you want to create a new configuration.
## Quick Start
.. column::
A simple example using default settings would be like this:
.. column::
```python
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
app = Sanic('logging_example')
@app.route('/')
async def test(request):
logger.info('Here is your log')
return text('Hello World!')
if __name__ == "__main__":
app.run(debug=True, access_log=True)
```
After the server is running, you should see logs like this.
```text
[2021-01-04 15:26:26 +0200] [1929659] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2021-01-04 15:26:26 +0200] [1929659] [INFO] Starting worker [1929659]
```
You can send a request to server and it will print the log messages.
```text
[2021-01-04 15:26:28 +0200] [1929659] [INFO] Here is your log
[2021-01-04 15:26:28 +0200] - (sanic.access)[INFO][127.0.0.1:44228]: GET http://localhost:8000/ 200 -1
```
## Changing Sanic loggers
To use your own logging config, simply use `logging.config.dictConfig`, or pass `log_config` when you initialize Sanic app.
```python
app = Sanic('logging_example', log_config=LOGGING_CONFIG)
if __name__ == "__main__":
app.run(access_log=False)
```
.. tip:: FYI
Logging in Python is a relatively cheap operation. However, if you are serving a high number of requests and performance is a concern, all of that time logging out access logs adds up and becomes quite expensive.
This is a good opportunity to place Sanic behind a proxy (like nginx) and to do your access logging there. You will see a *significant* increase in overall performance by disabling the `access_log`.
For optimal production performance, it is advised to run Sanic with `debug` and `access_log` disabled: `app.run(debug=False, access_log=False)`
## Configuration
Sanic's default logging configuration is: `sanic.log.LOGGING_CONFIG_DEFAULTS`.
.. column::
There are three loggers used in sanic, and must be defined if you want to create your own logging configuration:
| **Logger Name** | **Use Case** |
|-----------------|-------------------------------|
| `sanic.root` | Used to log internal messages. |
| `sanic.error` | Used to log error logs. |
| `sanic.access` | Used to log access logs. |
.. column::
### Log format
In addition to default parameters provided by Python (`asctime`, `levelname`, `message`), Sanic provides additional parameters for access logger with.
| Log Context Parameter | Parameter Value | Datatype |
|-----------------------|---------------------------------------|----------|
| `host` | `request.ip` | `str` |
| `request` | `request.method + " " + request.url` | `str` |
| `status` | `response` | `int` |
| `byte` | `len(response.body)` | `int` |
The default access log format is:
```text
%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d
```

View File

@ -1,3 +0,0 @@
# Testing
See [sanic-testing](../../plugins/sanic-testing/getting-started.md)

View File

@ -1,74 +0,0 @@
# Caddy Deployment
## Introduction
Caddy is a state-of-the-art web server and proxy that supports up to HTTP/3. Its simplicity lies in its minimalistic configuration and the inbuilt ability to automatically procure TLS certificates for your domains from Let's Encrypt. In this setup, we will configure the Sanic application to serve locally at 127.0.0.1:8001, with Caddy playing the role of the public-facing server for the domain example.com.
You may install Caddy from your favorite package menager on Windows, Linux and Mac. The package is named `caddy`.
## Proxied Sanic app
```python
from sanic import Sanic
from sanic.response import text
app = Sanic("proxied_example")
@app.get("/")
def index(request):
# This should display external (public) addresses:
return text(
f"{request.remote_addr} connected to {request.url_for('index')}\n"
f"Forwarded: {request.forwarded}\n"
)
```
To run this application, save as `proxied_example.py`, and use the sanic command-line interface as follows:
```bash
SANIC_PROXIES_COUNT=1 sanic proxied_example --port 8001
```
Setting the SANIC_PROXIES_COUNT environment variable instructs Sanic to trust the X-Forwarded-* headers sent by Caddy, allowing it to correctly identify the client's IP address and other information.
## Caddy is simple
If you have no other web servers running, you can simply run Caddy CLI (needs `sudo` on Linux):
```bash
caddy reverse-proxy --from example.com --to :8001
```
This is a complete server that includes a certificate for your domain, http-to-https redirect, proxy headers, streaming and WebSockets. Your Sanic application should now be available on the domain you specified by HTTP versions 1, 2 and 3. Remember to open up UDP/443 on your firewall to enable H3 communications.
All done?
Soon enough you'll be needing more than one server, or more control over details, which is where the configuration files come in. The above command is equivalent to this `Caddyfile`, serving as a good starting point for your install:
```
example.com {
reverse_proxy localhost:8001
}
```
Some Linux distributions install Caddy such that it reads configuration from `/etc/caddy/Caddyfile`, which `import /etc/caddy/conf.d/*` for each site you are running. If not, you'll need to manually run `caddy run` as a system service, pointing it at the proper config file. Alternatively, use Caddy API mode with `caddy run --resume` for persistent config changes. Note that any Caddyfile loading will replace all prior configuration and thus `caddy-api` is not configurable in this traditional manner.
## Advanced configuration
At times, you might need to mix static files and handlers at the site root for cleaner URLs. In Sanic, you'd use `app.static("/", "static", index="index.html")` to achieve this. However, for improved performance, you can offload serving static files to Caddy:
```
app.example.com {
# Look for static files first, proxy to Sanic if not found
route {
file_server {
root /srv/sanicexample/static
precompress br # brotli your large scripts and styles
pass_thru
}
reverse_proxy unix//tmp/sanic.socket # sanic --unix /tmp/sanic.socket
}
}
```
Please refer to [Caddy documentation](https://caddyserver.com/docs/) for more options.

View File

@ -1,175 +0,0 @@
# Docker Deployment
## Introduction
For a long time, the environment has always been a difficult problem for deployment. If there are conflicting configurations in your project, you have to spend a lot of time resolving them. Fortunately, virtualization provides us with a good solution. Docker is one of them. If you don't know Docker, you can visit [Docker official website](https://www.docker.com/) to learn more.
## Build Image
Let's start with a simple project. We will use a Sanic project as an example. Assume the project path is `/path/to/SanicDocker`.
.. column::
The directory structure looks like this:
.. column::
```text
# /path/to/SanicDocker
SanicDocker
├── requirements.txt
├── dockerfile
└── server.py
```
.. column::
And the `server.py` code looks like this:
.. column::
```python
app = Sanic("MySanicApp")
@app.get('/')
async def hello(request):
return text("OK!")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
```
.. note::
Please note that the host cannot be 127.0.0.1 . In docker container, 127.0.0.1 is the default network interface of the container, only the container can communicate with other containers. more information please visit [Docker network](https://docs.docker.com/engine/reference/commandline/network/)
Code is ready, let's write the `Dockerfile`:
```Dockerfile
FROM sanicframework/sanic:3.8-latest
WORKDIR /sanic
COPY . .
RUN pip install -r requirements.txt
EXPOSE 8000
CMD ["python", "server.py"]
```
Run the following command to build the image:
```shell
docker build -t my-sanic-image .
```
## Start Container
.. column::
After the image built, we can start the container use `my-sanic-image`:
.. column::
```shell
docker run --name mysanic -p 8000:8000 -d my-sanic-image
```
.. column::
Now we can visit `http://localhost:8000` to see the result:
.. column::
```text
OK!
```
## Use docker-compose
If your project consist of multiple services, you can use [docker-compose](https://docs.docker.com/compose/) to manage them.
for example, we will deploy `my-sanic-image` and `nginx`, achieve through nginx access sanic server.
.. column::
First of all, we need prepare nginx configuration file. create a file named `mysanic.conf`:
.. column::
```nginx
server {
listen 80;
listen [::]:80;
location / {
proxy_pass http://mysanic:8000/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Accept-Encoding gzip;
}
}
```
.. column::
Then, we need to prepare `docker-compose.yml` file. The content follows:
.. column::
```yaml
version: "3"
services:
mysanic:
image: my-sanic-image
ports:
- "8000:8000"
restart: always
mynginx:
image: nginx:1.13.6-alpine
ports:
- "80:80"
depends_on:
- mysanic
volumes:
- ./mysanic.conf:/etc/nginx/conf.d/mysanic.conf
restart: always
networks:
default:
driver: bridge
```
.. column::
After that, we can start them:
.. column::
```shell
docker-compose up -d
```
.. column::
Now, we can visit `http://localhost:80` to see the result:
.. column::
```text
OK!
```

View File

@ -1 +0,0 @@
# Kubernetes

View File

@ -1,172 +0,0 @@
# Nginx Deployment
## Introduction
Although Sanic can be run directly on Internet, it may be useful to use a proxy
server such as Nginx in front of it. This is particularly useful for running
multiple virtual hosts on the same IP, serving NodeJS or other services beside
a single Sanic app, and it also allows for efficient serving of static files.
TLS and HTTP/2 are also easily implemented on such proxy.
We are setting the Sanic app to serve only locally at 127.0.0.1:8001, while the
Nginx installation is responsible for providing the service to public Internet
on domain example.com. Static files will be served by Nginx for maximal
performance.
## Proxied Sanic app
```python
from sanic import Sanic
from sanic.response import text
app = Sanic("proxied_example")
@app.get("/")
def index(request):
# This should display external (public) addresses:
return text(
f"{request.remote_addr} connected to {request.url_for('index')}\n"
f"Forwarded: {request.forwarded}\n"
)
```
Since this is going to be a system service, save your code to
`/srv/sanicservice/proxied_example.py`.
For testing, run your app in a terminal using the `sanic` CLI in the folder where you saved the file.
```bash
SANIC_FORWARDED_SECRET=_hostname sanic proxied_example --port 8001
```
We provide Sanic config `FORWARDED_SECRET` to identify which proxy it gets
the remote addresses from. Note the `_` in front of the local hostname.
This gives basic protection against users spoofing these headers and faking
their IP addresses and more.
## SSL certificates
Install Certbot and obtain a certicate for all your domains. This will spin up its own webserver on port 80 for a moment to verify you control the given domain names.
```bash
certbot -d example.com -d www.example.com
```
## Nginx configuration
Quite much configuration is required to allow fast transparent proxying, but
for the most part these don't need to be modified, so bear with me.
.. tip:: Note
Separate upstream section, rather than simply adding the IP after `proxy_pass`
as in most tutorials, is needed for HTTP keep-alive. We also enable streaming,
WebSockets and Nginx serving static files.
The following config goes inside the `http` section of `nginx.conf` or if your
system uses multiple config files, `/etc/nginx/sites-available/default` or
your own files (be sure to symlink them to `sites-enabled`):
```nginx
# Files managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Sanic service
upstream example.com {
keepalive 100;
server 127.0.0.1:8001;
#server unix:/tmp//sanic.sock;
}
server {
server_name example.com;
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
# Serve static files if found, otherwise proxy to Sanic
location / {
root /srv/sanicexample/static;
try_files $uri @sanic;
}
location @sanic {
proxy_pass http://$server_name;
# Allow fast streaming HTTP/1.1 pipes (keep-alive, unbuffered)
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
proxy_set_header forwarded by=\"_$hostname\";$for_addr;proto=$scheme;host=\"$http_host\";
# Allow websockets and keep-alive (avoid connection: close)
proxy_set_header connection "upgrade";
proxy_set_header upgrade $http_upgrade;
}
}
# Redirect WWW to no-WWW
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ~^www\.(.*)$;
return 308 $scheme://$1$request_uri;
}
# Redirect all HTTP to HTTPS with no-WWW
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name ~^(?:www\.)?(.*)$;
return 308 https://$1$request_uri;
}
# Forwarded for= client IP address formatting
map $remote_addr $for_addr {
~^[0-9.]+$ "for=$remote_addr"; # IPv4 client address
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\""; # IPv6 bracketed and quoted
default "for=unknown"; # Unix socket
}
```
Start or restart Nginx for changes to take effect. E.g.
```bash
systemctl restart nginx
```
You should be able to connect your app on `https://example.com`. Any 404
errors and such will be handled by Sanic's error pages, and whenever a static
file is present at a given path, it will be served by Nginx.
## Running as a service
This part is for Linux distributions based on `systemd`. Create a unit file
`/etc/systemd/system/sanicexample.service`
```
[Unit]
Description=Sanic Example
[Service]
DynamicUser=Yes
WorkingDirectory=/srv/sanicservice
Environment=SANIC_PROXY_SECRET=_hostname
ExecStart=sanic proxied_example --port 8001 --fast
Restart=always
[Install]
WantedBy=multi-user.target
```
Then reload service files, start your service and enable it on boot:
```bash
systemctl daemon-reload
systemctl start sanicexample
systemctl enable sanicexample
```
.. tip:: Note
For brevity we skipped setting up a separate user account and a Python virtual environment or installing your app as a Python module. There are good tutorials on those topics elsewhere that easily apply to Sanic as well. The DynamicUser setting creates a strong sandbox which basically means your application cannot store its data in files, so you may consider setting `User=sanicexample` instead if you need that.

View File

@ -1,93 +0,0 @@
# Getting Started
Before we begin, make sure you are running Python 3.8 or higher. Currently, Sanic is works with Python versions 3.8 3.11.
## Install
```sh
pip install sanic
```
## Hello, world application
.. column::
If you have ever used one of the many decorator based frameworks, this probably looks somewhat familiar to you.
.. note::
If you are coming from Flask or another framework, there are a few important things to point out. Remember, Sanic aims for performance, flexibility, and ease of use. These guiding principles have tangible impact on the API and how it works.
.. column::
```python
from sanic import Sanic
from sanic.response import text
app = Sanic("MyHelloWorldApp")
@app.get("/")
async def hello_world(request):
return text("Hello, world.")
```
### Important to note
- Every request handler can either be sync (`def hello_world`) or async (`async def hello_world`). Unless you have a clear reason for it, always go with `async`.
- The `request` object is always the first argument of your handler. Other frameworks pass this around in a context variable to be imported. In the `async` world, this would not work so well and it is far easier (not to mention cleaner and more performant) to be explicit about it.
- You **must** use a response type. MANY other frameworks allow you to have a return value like this: `return "Hello, world."` or this: `return {"foo": "bar"}`. But, in order to do this implicit calling, somewhere in the chain needs to spend valuable time trying to determine what you meant. So, at the expense of this ease, Sanic has decided to require an explicit call.
### Running
.. column::
Let's save the above file as `server.py`. And launch it.
.. column::
```sh
sanic server
```
.. note::
This **another** important distinction. Other frameworks come with a built in development server and explicitly say that it is _only_ intended for development use. The opposite is true with Sanic.
**The packaged server is production ready.**
## Sanic Extensions
Sanic intentionally aims for a clean and unopinionated feature list. The project does not want to require you to build your application in a certain way, and tries to avoid prescribing specific development patterns. There are a number of third-party plugins that are built and maintained by the community to add additional features that do not otherwise meet the requirements of the core repository.
However, in order **to help API developers**, the Sanic organization maintains an official plugin called [Sanic Extensions](../plugins/sanic-ext/getting-started.md) to provide all sorts of goodies, including:
- **OpenAPI** documentation with Redoc and/or Swagger
- **CORS** protection
- **Dependency injection** into route handlers
- Request query arguments and body input **validation**
- Auto create `HEAD`, `OPTIONS`, and `TRACE` endpoints
- Predefined, endpoint-specific response serializers
The preferred method to set it up is to install it along with Sanic, but you can also install the packages on their own.
.. column::
```sh
pip install sanic[ext]
```
.. column::
```sh
pip install sanic sanic-ext
```
Starting in v21.12, Sanic will automatically setup Sanic Extensions if it is in the same environment. You will also have access to two additional application properties:
- `app.extend()` - used to configure Sanic Extensions
- `app.ext` - the `Extend` instance attached to the application
See [the plugin documentation](../plugins/sanic-ext/getting-started.md) for more information about how to use and work with the plugin

View File

@ -1 +0,0 @@
# How to ...

View File

@ -1,117 +0,0 @@
# Authentication
> How do I control authentication and authorization?
This is an _extremely_ complicated subject to cram into a few snippets. But, this should provide you with an idea on ways to tackle this problem. This example uses [JWTs](https://jwt.io/), but the concepts should be equally applicable to sessions or some other scheme.
## `server.py`
```python
from sanic import Sanic, text
from auth import protected
from login import login
app = Sanic("AuthApp")
app.config.SECRET = "KEEP_IT_SECRET_KEEP_IT_SAFE"
app.blueprint(login)
@app.get("/secret")
@protected
async def secret(request):
return text("To go fast, you must be fast.")
```
## `login.py`
```python
import jwt
from sanic import Blueprint, text
login = Blueprint("login", url_prefix="/login")
@login.post("/")
async def do_login(request):
token = jwt.encode({}, request.app.config.SECRET)
return text(token)
```
## `auth.py`
```python
from functools import wraps
import jwt
from sanic import text
def check_token(request):
if not request.token:
return False
try:
jwt.decode(
request.token, request.app.config.SECRET, algorithms=["HS256"]
)
except jwt.exceptions.InvalidTokenError:
return False
else:
return True
def protected(wrapped):
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
is_authenticated = check_token(request)
if is_authenticated:
response = await f(request, *args, **kwargs)
return response
else:
return text("You are unauthorized.", 401)
return decorated_function
return decorator(wrapped)
```
This decorator pattern is taken from the [decorators page](/en/guide/best-practices/decorators.md).
---
```bash
$ curl localhost:9999/secret -i
HTTP/1.1 401 Unauthorized
content-length: 21
connection: keep-alive
content-type: text/plain; charset=utf-8
You are unauthorized.
$ curl localhost:9999/login -X POST
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.rjxS7ztIGt5tpiRWS8BGLUqjQFca4QOetHcZTi061DE
$ curl localhost:9999/secret -i -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.rjxS7ztIGt5tpiRWS8BGLUqjQFca4QOetHcZTi061DE"
HTTP/1.1 200 OK
content-length: 29
connection: keep-alive
content-type: text/plain; charset=utf-8
To go fast, you must be fast.
$ curl localhost:9999/secret -i -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.BAD"
HTTP/1.1 401 Unauthorized
content-length: 21
connection: keep-alive
content-type: text/plain; charset=utf-8
You are unauthorized.
```
Also, checkout some resources from the community:
- Awesome Sanic - [Authorization](https://github.com/mekicha/awesome-sanic/blob/master/README.md#authentication) & [Session](https://github.com/mekicha/awesome-sanic/blob/master/README.md#session)
- [EuroPython 2020 - Overcoming access control in web APIs](https://www.youtube.com/watch?v=Uqgoj43ky6A)

View File

@ -1,197 +0,0 @@
---
title: Autodiscovery
---
# Autodiscovery of Blueprints, Middleware, and Listeners
> How do I autodiscover the components I am using to build my application?
One of the first problems someone faces when building an application, is *how* to structure the project. Sanic makes heavy use of decorators to register route handlers, middleware, and listeners. And, after creating blueprints, they need to be mounted to the application.
A possible solution is a single file in which **everything** is imported and applied to the Sanic instance. Another is passing around the Sanic instance as a global variable. Both of these solutions have their drawbacks.
An alternative is autodiscovery. You point your application at modules (already imported, or strings), and let it wire everything up.
## `server.py`
```python
from sanic import Sanic
from sanic.response import empty
import blueprints
from utility import autodiscover
app = Sanic("auto", register=True)
autodiscover(
app,
blueprints,
"parent.child",
"listeners.something",
recursive=True,
)
app.route("/")(lambda _: empty())
```
```bash
[2021-03-02 21:37:02 +0200] [880451] [INFO] Goin' Fast @ http://127.0.0.1:9999
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something @ nested
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something @ level1
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something @ level3
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something inside __init__.py
[2021-03-02 21:37:02 +0200] [880451] [INFO] Starting worker [880451]
```
## `utility.py`
```python
from glob import glob
from importlib import import_module, util
from inspect import getmembers
from pathlib import Path
from types import ModuleType
from typing import Union
from sanic.blueprints import Blueprint
def autodiscover(
app, *module_names: Union[str, ModuleType], recursive: bool = False
):
mod = app.__module__
blueprints = set()
_imported = set()
def _find_bps(module):
nonlocal blueprints
for _, member in getmembers(module):
if isinstance(member, Blueprint):
blueprints.add(member)
for module in module_names:
if isinstance(module, str):
module = import_module(module, mod)
_imported.add(module.__file__)
_find_bps(module)
if recursive:
base = Path(module.__file__).parent
for path in glob(f"{base}/**/*.py", recursive=True):
if path not in _imported:
name = "module"
if "__init__" in path:
*_, name, __ = path.split("/")
spec = util.spec_from_file_location(name, path)
specmod = util.module_from_spec(spec)
_imported.add(path)
spec.loader.exec_module(specmod)
_find_bps(specmod)
for bp in blueprints:
app.blueprint(bp)
```
## `blueprints/level1.py`
```python
from sanic import Blueprint
from sanic.log import logger
level1 = Blueprint("level1")
@level1.after_server_start
def print_something(app, loop):
logger.debug("something @ level1")
```
## `blueprints/one/two/level3.py`
```python
from sanic import Blueprint
from sanic.log import logger
level3 = Blueprint("level3")
@level3.after_server_start
def print_something(app, loop):
logger.debug("something @ level3")
```
## `listeners/something.py`
```python
from sanic import Sanic
from sanic.log import logger
app = Sanic.get_app("auto")
@app.after_server_start
def print_something(app, loop):
logger.debug("something")
```
## `parent/child/__init__.py`
```python
from sanic import Blueprint
from sanic.log import logger
bp = Blueprint("__init__")
@bp.after_server_start
def print_something(app, loop):
logger.debug("something inside __init__.py")
```
## `parent/child/nested.py`
```python
from sanic import Blueprint
from sanic.log import logger
nested = Blueprint("nested")
@nested.after_server_start
def print_something(app, loop):
logger.debug("something @ nested")
```
---
```text
here is the dir tree
generate with 'find . -type d -name "__pycache__" -exec rm -rf {} +; tree'
. # run 'sanic sever -d' here
├── blueprints
│ ├── __init__.py # you need add this file, just empty
│ ├── level1.py
│ └── one
│ └── two
│ └── level3.py
├── listeners
│ └── something.py
├── parent
│ └── child
│ ├── __init__.py
│ └── nested.py
├── server.py
└── utility.py
```
```sh
source ./.venv/bin/activate # activate the python venv which sanic is installed in
sanic sever -d # run this in the directory containing server.py
```
```text
you will see "something ***" like this:
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something inside __init__.py
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something @ level3
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something @ level1
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something @ nested
```

View File

@ -1,135 +0,0 @@
---
title: CORS
---
# Cross-origin resource sharing (CORS)
> How do I configure my application for CORS?
.. note::
🏆 The best solution is to use [Sanic Extensions](../../plugins/sanic-ext/http/cors.md).
However, if you would like to build your own version, you could use this limited example as a starting point.
### `server.py`
```python
from sanic import Sanic, text
from cors import add_cors_headers
from options import setup_options
app = Sanic("app")
@app.route("/", methods=["GET", "POST"])
async def do_stuff(request):
return text("...")
# Add OPTIONS handlers to any route that is missing it
app.register_listener(setup_options, "before_server_start")
# Fill in CORS headers
app.register_middleware(add_cors_headers, "response")
```
## `cors.py`
```python
from typing import Iterable
def _add_cors_headers(response, methods: Iterable[str]) -> None:
allow_methods = list(set(methods))
if "OPTIONS" not in allow_methods:
allow_methods.append("OPTIONS")
headers = {
"Access-Control-Allow-Methods": ",".join(allow_methods),
"Access-Control-Allow-Origin": "mydomain.com",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": (
"origin, content-type, accept, "
"authorization, x-xsrf-token, x-request-id"
),
}
response.headers.extend(headers)
def add_cors_headers(request, response):
if request.method != "OPTIONS":
methods = [method for method in request.route.methods]
_add_cors_headers(response, methods)
```
## `options.py`
```python
from collections import defaultdict
from typing import Dict, FrozenSet
from sanic import Sanic, response
from sanic.router import Route
from cors import _add_cors_headers
def _compile_routes_needing_options(
routes: Dict[str, Route]
) -> Dict[str, FrozenSet]:
needs_options = defaultdict(list)
# This is 21.12 and later. You will need to change this for older versions.
for route in routes.values():
if "OPTIONS" not in route.methods:
needs_options[route.uri].extend(route.methods)
return {
uri: frozenset(methods) for uri, methods in dict(needs_options).items()
}
def _options_wrapper(handler, methods):
def wrapped_handler(request, *args, **kwargs):
nonlocal methods
return handler(request, methods)
return wrapped_handler
async def options_handler(request, methods) -> response.HTTPResponse:
resp = response.empty()
_add_cors_headers(resp, methods)
return resp
def setup_options(app: Sanic, _):
app.router.reset()
needs_options = _compile_routes_needing_options(app.router.routes_all)
for uri, methods in needs_options.items():
app.add_route(
_options_wrapper(options_handler, methods),
uri,
methods=["OPTIONS"],
)
app.router.finalize()
```
---
```
$ curl localhost:9999/ -i
HTTP/1.1 200 OK
Access-Control-Allow-Methods: OPTIONS,POST,GET
Access-Control-Allow-Origin: mydomain.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: origin, content-type, accept, authorization, x-xsrf-token, x-request-id
content-length: 3
connection: keep-alive
content-type: text/plain; charset=utf-8
...
$ curl localhost:9999/ -i -X OPTIONS
HTTP/1.1 204 No Content
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Origin: mydomain.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: origin, content-type, accept, authorization, x-xsrf-token, x-request-id
connection: keep-alive
```
Also, checkout some resources from the community:
- [Awesome Sanic](https://github.com/mekicha/awesome-sanic/blob/master/README.md#frontend)

View File

@ -1 +0,0 @@
csrf

View File

@ -1 +0,0 @@
connecting to data sources

View File

@ -1 +0,0 @@
decorators

Some files were not shown because too many files have changed in this diff Show More