Compare commits
133 Commits
v21.3.4
...
pre-regist
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8fa52e2d2 | ||
|
|
266af1e279 | ||
|
|
6d3f1e9982 | ||
|
|
a0a3840094 | ||
|
|
f282865362 | ||
|
|
377c2ada38 | ||
|
|
264453459e | ||
|
|
3d383d7b97 | ||
|
|
c0cc26021b | ||
|
|
96c027bad5 | ||
|
|
b2a1bc69f5 | ||
|
|
426742b3e2 | ||
|
|
ab35121864 | ||
|
|
cf3c205fa5 | ||
|
|
19f6544923 | ||
|
|
f641830d26 | ||
|
|
a8d55e180c | ||
|
|
55c36e0240 | ||
|
|
2c03eee329 | ||
|
|
65e28b8c22 | ||
|
|
dfd33dd63d | ||
|
|
722a6db8d9 | ||
|
|
9c576c74db | ||
|
|
523db190a7 | ||
|
|
95631b9686 | ||
|
|
0860bfe1f1 | ||
|
|
85e7b712b9 | ||
|
|
b731a6b48c | ||
|
|
cde02b5936 | ||
|
|
abeb8d0bc0 | ||
|
|
9a9f72ad64 | ||
|
|
392a497366 | ||
|
|
36e6a6c506 | ||
|
|
a361b345ad | ||
|
|
f5bd6e3b2f | ||
|
|
6c7df68c7c | ||
|
|
5b82884f8b | ||
|
|
f0f81ec458 | ||
|
|
71cc30e5cd | ||
|
|
645310cff6 | ||
|
|
2f30b5748a | ||
|
|
5e1ef96934 | ||
|
|
57e98b62b3 | ||
|
|
3262878ebd | ||
|
|
5e12edbc38 | ||
|
|
50a606adee | ||
|
|
f995612073 | ||
|
|
bc08383acd | ||
|
|
b83a1a184c | ||
|
|
59dd6814f8 | ||
|
|
f7abf3db1b | ||
|
|
cf1d2148ac | ||
|
|
b5f2bd9b0e | ||
|
|
ba2670e99c | ||
|
|
6ffc4d9756 | ||
|
|
595d2c76ac | ||
|
|
d9796e9b1e | ||
|
|
404c5f9f9e | ||
|
|
a937e08ef0 | ||
|
|
ef4f058a6c | ||
|
|
69c5dde9bf | ||
|
|
945885d501 | ||
|
|
9d0b54c90d | ||
|
|
2e5c288fea | ||
|
|
f32ef20b74 | ||
|
|
e2eefaac55 | ||
|
|
e1cfbf0fd9 | ||
|
|
08c5689441 | ||
|
|
8dbda247d6 | ||
|
|
71a631237d | ||
|
|
e22ff3828b | ||
|
|
b1b12e004e | ||
|
|
5308fec354 | ||
|
|
0ba57d4701 | ||
|
|
54ca6a6178 | ||
|
|
7dd4a78cf2 | ||
|
|
52ff49512a | ||
|
|
5a48b94089 | ||
|
|
ba1c73d947 | ||
|
|
4732b6bdfa | ||
|
|
a6e78b70ab | ||
|
|
bb1174afc5 | ||
|
|
df8abe9cfd | ||
|
|
c3bca97ee1 | ||
|
|
c3b6fa1bba | ||
|
|
94d496afe1 | ||
|
|
7b7a572f9b | ||
|
|
1b8cb742f9 | ||
|
|
3492d180a8 | ||
|
|
021da38373 | ||
|
|
ac784759d5 | ||
|
|
36eda2cd62 | ||
|
|
08a4b3013f | ||
|
|
1dd0332e8b | ||
|
|
a90877ac31 | ||
|
|
8b7ea27a48 | ||
|
|
8df80e276b | ||
|
|
30572c972d | ||
|
|
53da4dd091 | ||
|
|
108a4a99c7 | ||
|
|
7c180376d6 | ||
|
|
f39b8b32f7 | ||
|
|
c543d19f8a | ||
|
|
80fca9aef7 | ||
|
|
5bb9aa0c2c | ||
|
|
83c746ee57 | ||
|
|
aff6604636 | ||
|
|
2c80571a8a | ||
|
|
d964b552af | ||
|
|
48f8b37b74 | ||
|
|
141be0028d | ||
|
|
a140c47195 | ||
|
|
0c3a8392f2 | ||
|
|
16875b1f41 | ||
|
|
b1f31f2eeb | ||
|
|
d16b9e5a02 | ||
|
|
680484bdc8 | ||
|
|
05cd44b5dd | ||
|
|
ba374139f4 | ||
|
|
72a745bfd5 | ||
|
|
3a6fac7d59 | ||
|
|
28ba8e53df | ||
|
|
9b26358e63 | ||
|
|
e21521f45c | ||
|
|
30479765cb | ||
|
|
53a571ec6c | ||
|
|
ad97cac313 | ||
|
|
1a352ddf55 | ||
|
|
5ba43decf2 | ||
|
|
8f06d035cb | ||
|
|
b716f48c84 | ||
|
|
42b1e7143e | ||
|
|
eba7821a6d |
28
.codeclimate.yml
Normal file
28
.codeclimate.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
exclude_patterns:
|
||||
- "sanic/__main__.py"
|
||||
- "sanic/application/logo.py"
|
||||
- "sanic/application/motd.py"
|
||||
- "sanic/reloader_helpers.py"
|
||||
- "sanic/simple.py"
|
||||
- "sanic/utils.py"
|
||||
- ".github/"
|
||||
- "changelogs/"
|
||||
- "docker/"
|
||||
- "docs/"
|
||||
- "examples/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
checks:
|
||||
argument-count:
|
||||
enabled: false
|
||||
file-lines:
|
||||
config:
|
||||
threshold: 1000
|
||||
method-count:
|
||||
config:
|
||||
threshold: 40
|
||||
complex-logic:
|
||||
enabled: false
|
||||
method-complexity:
|
||||
config:
|
||||
threshold: 10
|
||||
10
.coveragerc
10
.coveragerc
@@ -1,7 +1,15 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = sanic
|
||||
omit = site-packages, sanic/utils.py, sanic/__main__.py
|
||||
omit =
|
||||
site-packages
|
||||
sanic/application/logo.py
|
||||
sanic/application/motd.py
|
||||
sanic/cli
|
||||
sanic/__main__.py
|
||||
sanic/reloader_helpers.py
|
||||
sanic/simple.py
|
||||
sanic/utils.py
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
|
||||
39
.github/workflows/codeql-analysis.yml
vendored
39
.github/workflows/codeql-analysis.yml
vendored
@@ -1,27 +1,17 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
schedule:
|
||||
- cron: '25 16 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
if: github.event.pull_request.draft == false
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -29,39 +19,18 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
36
.github/workflows/coverage.yml
vendored
Normal file
36
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Coverage check
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "!*" # Do not execute on tags
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies 🔨
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- uses: paambaati/codeclimate-action@v2.5.3
|
||||
if: always()
|
||||
env:
|
||||
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
|
||||
with:
|
||||
coverageCommand: tox -e coverage
|
||||
39
.github/workflows/on-demand.yml
vendored
Normal file
39
.github/workflows/on-demand.yml
vendored
Normal 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"
|
||||
35
.github/workflows/pr-bandit.yml
vendored
Normal file
35
.github/workflows/pr-bandit.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Security Analysis
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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 }}"
|
||||
31
.github/workflows/pr-docs.yml
vendored
Normal file
31
.github/workflows/pr-docs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Document Linter
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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 }}"
|
||||
32
.github/workflows/pr-linter.yml
vendored
Normal file
32
.github/workflows/pr-linter.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Linter Checks
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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
41
.github/workflows/pr-python-pypy.yml
vendored
Normal 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
46
.github/workflows/pr-python310.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Python 3.10 Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
testPy310:
|
||||
if: github.event.pull_request.draft == false
|
||||
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"
|
||||
34
.github/workflows/pr-python37.yml
vendored
Normal file
34
.github/workflows/pr-python37.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Python 3.7 Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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"
|
||||
34
.github/workflows/pr-python38.yml
vendored
Normal file
34
.github/workflows/pr-python38.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Python 3.8 Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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"
|
||||
46
.github/workflows/pr-python39.yml
vendored
Normal file
46
.github/workflows/pr-python39.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Python 3.9 Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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"
|
||||
35
.github/workflows/pr-type-check.yml
vendored
Normal file
35
.github/workflows/pr-type-check.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Typing Checks
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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 }}"
|
||||
37
.github/workflows/pr-windows.yml
vendored
Normal file
37
.github/workflows/pr-windows.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Run Unit Tests on Windows
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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
48
.github/workflows/publish-images.yml
vendored
Normal 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
28
.github/workflows/publish-package.yml
vendored
Normal 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"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage
|
||||
coverage.xml
|
||||
.tox
|
||||
settings.py
|
||||
.idea/*
|
||||
@@ -18,3 +19,6 @@ build/*
|
||||
.DS_Store
|
||||
dist/*
|
||||
pip-wheel-metadata/
|
||||
.pytest_cache/*
|
||||
.venv/*
|
||||
.vscode/*
|
||||
|
||||
94
.travis.yml
94
.travis.yml
@@ -1,94 +0,0 @@
|
||||
sudo: false
|
||||
language: python
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
matrix:
|
||||
include:
|
||||
- env: TOX_ENV=py37
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 with Extensions"
|
||||
- env: TOX_ENV=py37-no-ext
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 without Extensions"
|
||||
- env: TOX_ENV=py38
|
||||
python: 3.8
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.8 with Extensions"
|
||||
- env: TOX_ENV=py38-no-ext
|
||||
python: 3.8
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.8 without Extensions"
|
||||
- env: TOX_ENV=py39
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
sudo: true
|
||||
name: "Python 3.9 with Extensions"
|
||||
- env: TOX_ENV=py39-no-ext
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
sudo: true
|
||||
name: "Python 3.9 without Extensions"
|
||||
- env: TOX_ENV=type-checking
|
||||
python: 3.7
|
||||
name: "Python 3.7 Type checks"
|
||||
- env: TOX_ENV=type-checking
|
||||
python: 3.8
|
||||
name: "Python 3.8 Type checks"
|
||||
- env: TOX_ENV=type-checking
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
name: "Python 3.9 Type checks"
|
||||
- env: TOX_ENV=security
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 Bandit security scan"
|
||||
- env: TOX_ENV=security
|
||||
python: 3.8
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.8 Bandit security scan"
|
||||
- env: TOX_ENV=security
|
||||
python: 3.9
|
||||
dist: bionic
|
||||
sudo: true
|
||||
name: "Python 3.9 Bandit security scan"
|
||||
- env: TOX_ENV=docs
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
name: "Python 3.7 Documentation tests"
|
||||
- env: TOX_ENV=pyNightly
|
||||
python: "nightly"
|
||||
name: "Python nightly with Extensions"
|
||||
- env: TOX_ENV=pyNightly-no-ext
|
||||
python: "nightly"
|
||||
name: "Python nightly without Extensions"
|
||||
allow_failures:
|
||||
- env: TOX_ENV=pyNightly
|
||||
python: "nightly"
|
||||
name: "Python nightly with Extensions"
|
||||
- env: TOX_ENV=pyNightly-no-ext
|
||||
python: "nightly"
|
||||
name: "Python nightly without Extensions"
|
||||
install:
|
||||
- pip install -U tox
|
||||
- pip install codecov
|
||||
script: travis_retry tox -e $TOX_ENV
|
||||
after_success:
|
||||
- codecov
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: brewmaster
|
||||
password:
|
||||
secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys="
|
||||
on:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
117
CHANGELOG.rst
117
CHANGELOG.rst
@@ -1,3 +1,114 @@
|
||||
.. note::
|
||||
|
||||
From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases``
|
||||
|
||||
Version 21.6.1
|
||||
--------------
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
|
||||
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
|
||||
Update sanic-routing to allow for better splitting of complex URI templates
|
||||
* `#2183 <https://github.com/sanic-org/sanic/pull/2183>`_
|
||||
Proper handling of chunked request bodies to resolve phantom 503 in logs
|
||||
* `#2181 <https://github.com/sanic-org/sanic/pull/2181>`_
|
||||
Resolve regression in exception logging
|
||||
* `#2201 <https://github.com/sanic-org/sanic/pull/2201>`_
|
||||
Cleanup request info in pipelined requests
|
||||
|
||||
Version 21.6.0
|
||||
--------------
|
||||
|
||||
Features
|
||||
********
|
||||
|
||||
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
|
||||
Add ``response.eof()`` method for closing a stream in a handler
|
||||
* `#2097 <https://github.com/sanic-org/sanic/pull/2097>`_
|
||||
Allow case-insensitive HTTP Upgrade header
|
||||
* `#2104 <https://github.com/sanic-org/sanic/pull/2104>`_
|
||||
Explicit usage of CIMultiDict getters
|
||||
* `#2109 <https://github.com/sanic-org/sanic/pull/2109>`_
|
||||
Consistent use of error loggers
|
||||
* `#2114 <https://github.com/sanic-org/sanic/pull/2114>`_
|
||||
New ``client_ip`` access of connection info instance
|
||||
* `#2119 <https://github.com/sanic-org/sanic/pull/2119>`_
|
||||
Alternatate classes on instantiation for ``Config`` and ``Sanic.ctx``
|
||||
* `#2133 <https://github.com/sanic-org/sanic/pull/2133>`_
|
||||
Implement new version of AST router
|
||||
|
||||
* Proper differentiation between ``alpha`` and ``string`` param types
|
||||
* Adds a ``slug`` param type, example: ``<foo:slug>``
|
||||
* Deprecates ``<foo:string>`` in favor of ``<foo:str>``
|
||||
* Deprecates ``<foo:number>`` in favor of ``<foo:float>``
|
||||
* Adds a ``route.uri`` accessor
|
||||
* `#2136 <https://github.com/sanic-org/sanic/pull/2136>`_
|
||||
CLI improvements with new optional params
|
||||
* `#2137 <https://github.com/sanic-org/sanic/pull/2137>`_
|
||||
Add ``version_prefix`` to URL builders
|
||||
* `#2140 <https://github.com/sanic-org/sanic/pull/2140>`_
|
||||
Event autoregistration with ``EVENT_AUTOREGISTER``
|
||||
* `#2146 <https://github.com/sanic-org/sanic/pull/2146>`_, `#2147 <https://github.com/sanic-org/sanic/pull/2147>`_
|
||||
Require stricter names on ``Sanic()`` and ``Blueprint()``
|
||||
* `#2150 <https://github.com/sanic-org/sanic/pull/2150>`_
|
||||
Infinitely reusable and nestable ``Blueprint`` and ``BlueprintGroup``
|
||||
* `#2154 <https://github.com/sanic-org/sanic/pull/2154>`_
|
||||
Upgrade ``websockets`` dependency to min version
|
||||
* `#2155 <https://github.com/sanic-org/sanic/pull/2155>`_
|
||||
Allow for maximum header sizes to be increased: ``REQUEST_MAX_HEADER_SIZE``
|
||||
* `#2157 <https://github.com/sanic-org/sanic/pull/2157>`_
|
||||
Allow app factory pattern in CLI
|
||||
* `#2165 <https://github.com/sanic-org/sanic/pull/2165>`_
|
||||
Change HTTP methods to enums
|
||||
* `#2167 <https://github.com/sanic-org/sanic/pull/2167>`_
|
||||
Allow auto-reloading on additional directories
|
||||
* `#2168 <https://github.com/sanic-org/sanic/pull/2168>`_
|
||||
Add simple HTTP server to CLI
|
||||
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||
Additional methods for attaching ``HTTPMethodView``
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
|
||||
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
|
||||
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
|
||||
* `#2099 <https://github.com/sanic-org/sanic/pull/2099>`_
|
||||
Fix static request handler logging exception on 404
|
||||
* `#2110 <https://github.com/sanic-org/sanic/pull/2110>`_
|
||||
Fix request.args.pop removes parameters inconsistently
|
||||
* `#2107 <https://github.com/sanic-org/sanic/pull/2107>`_
|
||||
Fix type hinting for load_env
|
||||
* `#2127 <https://github.com/sanic-org/sanic/pull/2127>`_
|
||||
Make sure ASGI ws subprotocols is a list
|
||||
* `#2128 <https://github.com/sanic-org/sanic/pull/2128>`_
|
||||
Fix issue where Blueprint exception handlers do not consistently route to proper handler
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
*************************
|
||||
|
||||
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
|
||||
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
|
||||
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||
``CompositionView`` deprecated and marked for removal in 21.12
|
||||
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||
Deprecate StreamingHTTPResponse
|
||||
|
||||
Developer infrastructure
|
||||
************************
|
||||
|
||||
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
|
||||
Remove Travis CI in favor of GitHub Actions
|
||||
|
||||
Improved Documentation
|
||||
**********************
|
||||
|
||||
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
|
||||
Fix typo in documentation
|
||||
* `#2100 <https://github.com/sanic-org/sanic/pull/2100>`_
|
||||
Remove documentation for non-existent arguments
|
||||
|
||||
Version 21.3.2
|
||||
--------------
|
||||
|
||||
@@ -546,7 +657,7 @@ Improved Documentation
|
||||
Version 20.6.0
|
||||
---------------
|
||||
|
||||
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
|
||||
*Released, but unintentionally omitting PR #1880, so was replaced by 20.6.1*
|
||||
|
||||
|
||||
Version 20.3.0
|
||||
@@ -979,7 +1090,7 @@ Version 18.12
|
||||
* Fix Range header handling for static files (#1402)
|
||||
* Fix the logger and make it work (#1397)
|
||||
* Fix type pikcle->pickle in multiprocessing test
|
||||
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirement of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||
* Fix document for logging
|
||||
|
||||
Version 0.8
|
||||
@@ -1018,7 +1129,7 @@ Version 0.8
|
||||
* Content-length header on 204/304 responses (Arnulfo Solís)
|
||||
* Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
||||
* Update development status from pre-alpha to beta (Maksim Anisenkov)
|
||||
* KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
||||
* KeepAlive Timeout log level changed to debug (Arnulfo Solís)
|
||||
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
||||
* Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
||||
* Add support for blueprint groups and nesting (Elias Tarhini)
|
||||
|
||||
@@ -19,7 +19,7 @@ a virtual environment already set up, then run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e . ".[dev]"
|
||||
pip install -e ".[dev]"
|
||||
|
||||
Dependency Changes
|
||||
------------------
|
||||
@@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks.
|
||||
tox -e lint
|
||||
|
||||
Run type annotation checks
|
||||
---------------
|
||||
--------------------------
|
||||
|
||||
``tox`` environment -> ``[testenv:type-checking]``
|
||||
|
||||
|
||||
6
Makefile
6
Makefile
@@ -49,6 +49,9 @@ test: clean
|
||||
test-coverage: clean
|
||||
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
|
||||
|
||||
view-coverage:
|
||||
sanic ./coverage --simple
|
||||
|
||||
install:
|
||||
python setup.py install
|
||||
|
||||
@@ -85,8 +88,7 @@ docs-test: docs-clean
|
||||
cd docs && make dummy
|
||||
|
||||
docs-serve:
|
||||
# python -m http.server --directory=./docs/_build/html 9999
|
||||
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
|
||||
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./
|
||||
|
||||
changelog:
|
||||
python scripts/changelog.py
|
||||
|
||||
31
README.rst
31
README.rst
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
||||
:stub-columns: 1
|
||||
|
||||
* - Build
|
||||
- | |Build Status| |AppVeyor Build Status| |Codecov|
|
||||
- | |Py39Test| |Py38Test| |Py37Test|
|
||||
* - Docs
|
||||
- | |UserGuide| |Documentation|
|
||||
* - Package
|
||||
@@ -27,12 +27,12 @@ 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
|
||||
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/sanic-org/sanic
|
||||
.. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master
|
||||
:target: https://travis-ci.com/sanic-org/sanic
|
||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||
:target: https://ci.appveyor.com/project/sanic-org/sanic
|
||||
.. |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
|
||||
@@ -75,17 +75,11 @@ The goal of the project is to provide a simple way to get up and running a highl
|
||||
Sponsor
|
||||
-------
|
||||
|
||||
|Try CodeStream|
|
||||
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
||||
|
||||
.. |Try CodeStream| image:: https://alt-images.codestream.com/codestream_logo_sanicorg.png
|
||||
:target: https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner
|
||||
:alt: Try CodeStream
|
||||
Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic.
|
||||
|
||||
Manage pull requests and conduct code reviews in your IDE with full source-tree context. Comment on any line, not just the diffs. Use jump-to-definition, your favorite keybindings, and code intelligence with more of your workflow.
|
||||
|
||||
`Learn More <https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner>`_
|
||||
|
||||
Thank you to our sponsor. Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
||||
|Linode|
|
||||
|
||||
Installation
|
||||
------------
|
||||
@@ -170,3 +164,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
|
||||
|
||||
14
codecov.yml
14
codecov.yml
@@ -1,14 +0,0 @@
|
||||
codecov:
|
||||
require_ci_to_pass: no
|
||||
coverage:
|
||||
precision: 3
|
||||
round: nearest
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.75%
|
||||
@@ -1,28 +1,9 @@
|
||||
FROM alpine:3.7
|
||||
ARG BASE_IMAGE_TAG
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
curl \
|
||||
bash \
|
||||
build-base \
|
||||
ca-certificates \
|
||||
git \
|
||||
bzip2-dev \
|
||||
linux-headers \
|
||||
ncurses-dev \
|
||||
openssl \
|
||||
openssl-dev \
|
||||
readline-dev \
|
||||
sqlite-dev
|
||||
FROM sanicframework/sanic-build:${BASE_IMAGE_TAG}
|
||||
|
||||
RUN apk update
|
||||
RUN update-ca-certificates
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
ENV PYENV_ROOT="/root/.pyenv"
|
||||
ENV PATH="$PYENV_ROOT/bin:$PATH"
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4
|
||||
|
||||
ENTRYPOINT ["./docker/bin/entrypoint.sh"]
|
||||
RUN pip install sanic
|
||||
RUN apk del build-base
|
||||
|
||||
9
docker/Dockerfile-base
Normal file
9
docker/Dockerfile-base
Normal file
@@ -0,0 +1,9 @@
|
||||
ARG PYTHON_VERSION
|
||||
|
||||
FROM python:${PYTHON_VERSION}-alpine
|
||||
RUN apk update
|
||||
RUN apk add --no-cache --update build-base \
|
||||
ca-certificates \
|
||||
openssl
|
||||
RUN update-ca-certificates
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
source /root/.pyenv/completions/pyenv.bash
|
||||
|
||||
pip install tox
|
||||
|
||||
exec $@
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export CFLAGS='-O2'
|
||||
export EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000"
|
||||
|
||||
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
|
||||
eval "$(pyenv init -)"
|
||||
|
||||
for ver in $@
|
||||
do
|
||||
pyenv install $ver
|
||||
done
|
||||
|
||||
pyenv global $@
|
||||
pip install --upgrade pip
|
||||
pyenv rehash
|
||||
20
docs/conf.py
20
docs/conf.py
@@ -10,10 +10,8 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add support for auto-doc
|
||||
import recommonmark
|
||||
|
||||
from recommonmark.transform import AutoStructify
|
||||
# Add support for auto-doc
|
||||
|
||||
|
||||
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
||||
@@ -26,7 +24,7 @@ import sanic
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = ["sphinx.ext.autodoc", "recommonmark"]
|
||||
extensions = ["sphinx.ext.autodoc", "m2r2"]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
|
||||
@@ -162,20 +160,6 @@ autodoc_default_options = {
|
||||
"member-order": "groupwise",
|
||||
}
|
||||
|
||||
|
||||
# app setup hook
|
||||
def setup(app):
|
||||
app.add_config_value(
|
||||
"recommonmark_config",
|
||||
{
|
||||
"enable_eval_rst": True,
|
||||
"enable_auto_doc_ref": False,
|
||||
},
|
||||
True,
|
||||
)
|
||||
app.add_transform(AutoStructify)
|
||||
|
||||
|
||||
html_theme_options = {
|
||||
"style_external_links": False,
|
||||
}
|
||||
|
||||
17
docs/sanic/api/app.rst
Normal file
17
docs/sanic/api/app.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
Application
|
||||
===========
|
||||
|
||||
sanic.app
|
||||
---------
|
||||
|
||||
.. automodule:: sanic.app
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.config
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.config
|
||||
:members:
|
||||
:show-inheritance:
|
||||
17
docs/sanic/api/blueprints.rst
Normal file
17
docs/sanic/api/blueprints.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
Blueprints
|
||||
==========
|
||||
|
||||
sanic.blueprints
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.blueprints
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.blueprint_group
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.blueprint_group
|
||||
:members:
|
||||
:special-members:
|
||||
47
docs/sanic/api/core.rst
Normal file
47
docs/sanic/api/core.rst
Normal file
@@ -0,0 +1,47 @@
|
||||
Core
|
||||
====
|
||||
|
||||
sanic.cookies
|
||||
-------------
|
||||
|
||||
.. automodule:: sanic.cookies
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
sanic.handlers
|
||||
--------------
|
||||
|
||||
.. automodule:: sanic.handlers
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
sanic.request
|
||||
-------------
|
||||
|
||||
.. automodule:: sanic.request
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.response
|
||||
--------------
|
||||
|
||||
.. automodule:: sanic.response
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
sanic.views
|
||||
-----------
|
||||
|
||||
.. automodule:: sanic.views
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.websocket
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic.websocket
|
||||
:members:
|
||||
:show-inheritance:
|
||||
16
docs/sanic/api/exceptions.rst
Normal file
16
docs/sanic/api/exceptions.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
sanic.errorpages
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.errorpages
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.exceptions
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.exceptions
|
||||
:members:
|
||||
:show-inheritance:
|
||||
18
docs/sanic/api/router.rst
Normal file
18
docs/sanic/api/router.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
Routing
|
||||
=======
|
||||
|
||||
sanic_routing models
|
||||
--------------------
|
||||
|
||||
.. autoclass:: sanic_routing.route::Route
|
||||
:members:
|
||||
|
||||
.. autoclass:: sanic_routing.group::RouteGroup
|
||||
:members:
|
||||
|
||||
sanic.router
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.router
|
||||
:members:
|
||||
:show-inheritance:
|
||||
25
docs/sanic/api/server.rst
Normal file
25
docs/sanic/api/server.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
Sanic Server
|
||||
============
|
||||
|
||||
sanic.http
|
||||
----------
|
||||
|
||||
.. automodule:: sanic.http
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
sanic.server
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.server
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
sanic.worker
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.worker
|
||||
:members:
|
||||
:show-inheritance:
|
||||
16
docs/sanic/api/utility.rst
Normal file
16
docs/sanic/api/utility.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
Utility
|
||||
=======
|
||||
|
||||
sanic.compat
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.compat
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.log
|
||||
---------
|
||||
|
||||
.. automodule:: sanic.log
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -1,132 +1,13 @@
|
||||
📑 API Reference
|
||||
================
|
||||
|
||||
sanic.app
|
||||
---------
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
.. automodule:: sanic.app
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.blueprints
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.blueprints
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
sanic.blueprint_group
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.blueprint_group
|
||||
:members:
|
||||
:special-members:
|
||||
|
||||
|
||||
sanic.compat
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.compat
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.config
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.config
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.cookies
|
||||
-------------
|
||||
|
||||
.. automodule:: sanic.cookies
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.errorpages
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.errorpages
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.exceptions
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.exceptions
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.handlers
|
||||
--------------
|
||||
|
||||
.. automodule:: sanic.handlers
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.http
|
||||
----------
|
||||
|
||||
.. automodule:: sanic.http
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.log
|
||||
---------
|
||||
|
||||
.. automodule:: sanic.log
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.request
|
||||
-------------
|
||||
|
||||
.. automodule:: sanic.request
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.response
|
||||
--------------
|
||||
|
||||
.. automodule:: sanic.response
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.router
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.router
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.server
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.server
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
sanic.views
|
||||
-----------
|
||||
|
||||
.. automodule:: sanic.views
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.websocket
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic.websocket
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.worker
|
||||
------------
|
||||
|
||||
.. automodule:: sanic.worker
|
||||
:members:
|
||||
:show-inheritance:
|
||||
api/app
|
||||
api/blueprints
|
||||
api/core
|
||||
api/exceptions
|
||||
api/router
|
||||
api/server
|
||||
api/utility
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
📜 Changelog
|
||||
============
|
||||
|
||||
.. mdinclude:: ./releases/21.9.md
|
||||
|
||||
.. include:: ../../CHANGELOG.rst
|
||||
|
||||
40
docs/sanic/releases/21.9.md
Normal file
40
docs/sanic/releases/21.9.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Version 21.9
|
||||
|
||||
### Features
|
||||
- [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets
|
||||
- [#2160](https://github.com/sanic-org/sanic/pull/2160) Add new 17 signals into server and request lifecycles
|
||||
- [#2162](https://github.com/sanic-org/sanic/pull/2162) Smarter `auto` fallback formatting upon exception
|
||||
- [#2184](https://github.com/sanic-org/sanic/pull/2184) Introduce implementation for copying a Blueprint
|
||||
- [#2200](https://github.com/sanic-org/sanic/pull/2200) Accept header parsing
|
||||
- [#2207](https://github.com/sanic-org/sanic/pull/2207) Log remote address if available
|
||||
- [#2209](https://github.com/sanic-org/sanic/pull/2209) Add convenience methods to BP groups
|
||||
- [#2216](https://github.com/sanic-org/sanic/pull/2216) Add default messages to SanicExceptions
|
||||
- [#2225](https://github.com/sanic-org/sanic/pull/2225) Type annotation convenience for annotated handlers with path parameters
|
||||
- [#2236](https://github.com/sanic-org/sanic/pull/2236) Allow Falsey (but not-None) responses from route handlers
|
||||
- [#2238](https://github.com/sanic-org/sanic/pull/2238) Add `exception` decorator to Blueprint Groups
|
||||
- [#2244](https://github.com/sanic-org/sanic/pull/2244) Explicit static directive for serving file or dir (ex: `static(..., resource_type="file")`)
|
||||
- [#2245](https://github.com/sanic-org/sanic/pull/2245) Close HTTP loop when connection task cancelled
|
||||
|
||||
### Bugfixes
|
||||
- [#2188](https://github.com/sanic-org/sanic/pull/2188) Fix the handling of the end of a chunked request
|
||||
- [#2195](https://github.com/sanic-org/sanic/pull/2195) Resolve unexpected error handling on static requests
|
||||
- [#2208](https://github.com/sanic-org/sanic/pull/2208) Make blueprint-based exceptions attach and trigger in a more intuitive manner
|
||||
- [#2211](https://github.com/sanic-org/sanic/pull/2211) Fixed for handling exceptions of asgi app call
|
||||
- [#2213](https://github.com/sanic-org/sanic/pull/2213) Fix bug where ws exceptions not being logged
|
||||
- [#2231](https://github.com/sanic-org/sanic/pull/2231) Cleaner closing of tasks by using `abort()` in strategic places to avoid dangling sockets
|
||||
- [#2247](https://github.com/sanic-org/sanic/pull/2247) Fix logging of auto-reload status in debug mode
|
||||
- [#2246](https://github.com/sanic-org/sanic/pull/2246) Account for BP with exception handler but no routes
|
||||
|
||||
### Developer infrastructure
|
||||
- [#2194](https://github.com/sanic-org/sanic/pull/2194) HTTP unit tests with raw client
|
||||
- [#2199](https://github.com/sanic-org/sanic/pull/2199) Switch to codeclimate
|
||||
- [#2214](https://github.com/sanic-org/sanic/pull/2214) Try Reopening Windows Tests
|
||||
- [#2229](https://github.com/sanic-org/sanic/pull/2229) Refactor `HttpProtocol` into a base class
|
||||
- [#2230](https://github.com/sanic-org/sanic/pull/2230) Refactor `server.py` into multi-file module
|
||||
|
||||
### Miscellaneous
|
||||
- [#2173](https://github.com/sanic-org/sanic/pull/2173) Remove Duplicated Dependencies and PEP 517 Support
|
||||
- [#2193](https://github.com/sanic-org/sanic/pull/2193), [#2196](https://github.com/sanic-org/sanic/pull/2196), [#2217](https://github.com/sanic-org/sanic/pull/2217) Type annotation changes
|
||||
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import asyncio
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic()
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
async def notify_server_started_after_five_seconds():
|
||||
await asyncio.sleep(5)
|
||||
print('Server successfully started!')
|
||||
print("Server successfully started!")
|
||||
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds())
|
||||
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from random import randint
|
||||
|
||||
app = Sanic()
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
@app.middleware('request')
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.middleware("request")
|
||||
def append_request(request):
|
||||
# Add new key with random value
|
||||
request['num'] = randint(0, 100)
|
||||
request.ctx.num = randint(0, 100)
|
||||
|
||||
|
||||
@app.get('/pop')
|
||||
@app.get("/pop")
|
||||
def pop_handler(request):
|
||||
# Pop key from request object
|
||||
num = request.pop('num')
|
||||
return text(num)
|
||||
return text(request.ctx.num)
|
||||
|
||||
|
||||
@app.get('/key_exist')
|
||||
@app.get("/key_exist")
|
||||
def key_exist_handler(request):
|
||||
# Check the key is exist or not
|
||||
if 'num' in request:
|
||||
return text('num exist in request')
|
||||
if hasattr(request.ctx, "num"):
|
||||
return text("num exist in request")
|
||||
|
||||
return text('num does not exist in reqeust')
|
||||
return text("num does not exist in request")
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sanic import Sanic
|
||||
from functools import wraps
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
def check_request_for_authorization_status(request):
|
||||
@@ -27,14 +29,16 @@ def authorized(f):
|
||||
return response
|
||||
else:
|
||||
# the user is not authorized.
|
||||
return json({'status': 'not_authorized'}, 403)
|
||||
return json({"status": "not_authorized"}, 403)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@authorized
|
||||
async def test(request):
|
||||
return json({'status': 'authorized'})
|
||||
return json({"status": "authorized"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1,43 +1,53 @@
|
||||
from sanic import Sanic, Blueprint
|
||||
from sanic import Blueprint, Sanic
|
||||
from sanic.response import text
|
||||
'''
|
||||
Demonstrates that blueprint request middleware are executed in the order they
|
||||
|
||||
|
||||
"""
|
||||
Demonstrates that blueprint request middleware are executed in the order they
|
||||
are added. And blueprint response middleware are executed in _reverse_ order.
|
||||
On a valid request, it should print "1 2 3 6 5 4" to terminal
|
||||
'''
|
||||
"""
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
bp = Blueprint("bp_"+__name__)
|
||||
bp = Blueprint("bp_" + __name__)
|
||||
|
||||
@bp.middleware('request')
|
||||
|
||||
@bp.on_request
|
||||
def request_middleware_1(request):
|
||||
print('1')
|
||||
print("1")
|
||||
|
||||
@bp.middleware('request')
|
||||
|
||||
@bp.on_request
|
||||
def request_middleware_2(request):
|
||||
print('2')
|
||||
print("2")
|
||||
|
||||
@bp.middleware('request')
|
||||
|
||||
@bp.on_request
|
||||
def request_middleware_3(request):
|
||||
print('3')
|
||||
print("3")
|
||||
|
||||
@bp.middleware('response')
|
||||
|
||||
@bp.on_response
|
||||
def resp_middleware_4(request, response):
|
||||
print('4')
|
||||
print("4")
|
||||
|
||||
@bp.middleware('response')
|
||||
|
||||
@bp.on_response
|
||||
def resp_middleware_5(request, response):
|
||||
print('5')
|
||||
print("5")
|
||||
|
||||
@bp.middleware('response')
|
||||
|
||||
@bp.on_response
|
||||
def resp_middleware_6(request, response):
|
||||
print('6')
|
||||
print("6")
|
||||
|
||||
@bp.route('/')
|
||||
|
||||
@bp.route("/")
|
||||
def pop_handler(request):
|
||||
return text('hello world')
|
||||
return text("hello world")
|
||||
|
||||
app.blueprint(bp, url_prefix='/bp')
|
||||
|
||||
app.blueprint(bp, url_prefix="/bp")
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic import Blueprint, Sanic
|
||||
from sanic.response import file, json
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
blueprint = Blueprint("name", url_prefix="/my_blueprint")
|
||||
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
|
||||
|
||||
@@ -2,17 +2,20 @@ from asyncio import sleep
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__, strict_slashes=True)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request):
|
||||
return response.redirect("/sleep/3")
|
||||
|
||||
|
||||
@app.get("/sleep/<t:number>")
|
||||
async def handler2(request, t=0.3):
|
||||
await sleep(t)
|
||||
return response.text(f"Slept {t:.1f} seconds.\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -7,8 +7,10 @@ and pass in an instance of it when we create our Sanic instance. Inside this
|
||||
class' default handler, we can do anything including sending exceptions to
|
||||
an external service.
|
||||
"""
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
|
||||
"""
|
||||
Imports and code relevant for our CustomHandler class
|
||||
(Ordinarily this would be in a separate file)
|
||||
@@ -16,7 +18,6 @@ Imports and code relevant for our CustomHandler class
|
||||
|
||||
|
||||
class CustomHandler(ErrorHandler):
|
||||
|
||||
def default(self, request, exception):
|
||||
# Here, we have access to the exception object
|
||||
# and can do anything with it (log, send to external service, etc)
|
||||
@@ -38,17 +39,17 @@ server's error_handler to an instance of our CustomHandler
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
handler = CustomHandler()
|
||||
app.error_handler = handler
|
||||
app = Sanic(__name__, error_handler=handler)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
# Here, something occurs which causes an unexpected exception
|
||||
# This exception will flow to our custom handler.
|
||||
raise SanicException('You Broke It!')
|
||||
raise SanicException("You Broke It!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from sanic import Sanic, response, text
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
|
||||
|
||||
HTTP_PORT = 9999
|
||||
@@ -32,20 +34,40 @@ def proxy(request, path):
|
||||
return response.redirect(url)
|
||||
|
||||
|
||||
@https.listener("main_process_start")
|
||||
@https.main_process_start
|
||||
async def start(app, _):
|
||||
global http
|
||||
app.http_server = await http.create_server(
|
||||
http_server = await http.create_server(
|
||||
port=HTTP_PORT, return_asyncio_server=True
|
||||
)
|
||||
app.http_server.after_start()
|
||||
app.add_task(runner(http, http_server))
|
||||
app.ctx.http_server = http_server
|
||||
app.ctx.http = http
|
||||
|
||||
|
||||
@https.listener("main_process_stop")
|
||||
@https.main_process_stop
|
||||
async def stop(app, _):
|
||||
app.http_server.before_stop()
|
||||
await app.http_server.close()
|
||||
app.http_server.after_stop()
|
||||
await app.ctx.http_server.before_stop()
|
||||
await app.ctx.http_server.close()
|
||||
for connection in app.ctx.http_server.connections:
|
||||
connection.close_if_idle()
|
||||
await app.ctx.http_server.after_stop()
|
||||
app.ctx.http = False
|
||||
|
||||
|
||||
async def runner(app: Sanic, app_server: AsyncioServer):
|
||||
app.is_running = True
|
||||
try:
|
||||
app.signalize()
|
||||
app.finalize()
|
||||
ErrorHandler.finalize(app.error_handler)
|
||||
app_server.init = True
|
||||
|
||||
await app_server.before_start()
|
||||
await app_server.after_start()
|
||||
await app_server.serve_forever()
|
||||
finally:
|
||||
app.is_running = False
|
||||
app.is_stopping = True
|
||||
|
||||
|
||||
https.run(port=HTTPS_PORT, debug=True)
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
sem = None
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def init(sanic, loop):
|
||||
@app.before_server_start
|
||||
def init(sanic, _):
|
||||
global sem
|
||||
concurrency_per_worker = 4
|
||||
sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
|
||||
sem = asyncio.Semaphore(concurrency_per_worker)
|
||||
|
||||
|
||||
async def bounded_fetch(session, url):
|
||||
"""
|
||||
Use session object to perform 'get' request on url
|
||||
"""
|
||||
async with sem, session.get(url) as response:
|
||||
return await response.json()
|
||||
async with sem:
|
||||
response = await session.get(url)
|
||||
return response.json()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@@ -28,9 +32,9 @@ async def test(request):
|
||||
"""
|
||||
Download and serve example JSON
|
||||
"""
|
||||
url = "https://api.github.com/repos/channelcat/sanic"
|
||||
url = "https://api.github.com/repos/sanic-org/sanic"
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with httpx.AsyncClient() as session:
|
||||
response = await bounded_fetch(session, url)
|
||||
return json(response)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
import aiotask_context as context
|
||||
from contextvars import ContextVar
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
@@ -11,8 +11,8 @@ log = logging.getLogger(__name__)
|
||||
class RequestIdFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
try:
|
||||
record.request_id = context.get("X-Request-ID")
|
||||
except ValueError:
|
||||
record.request_id = app.ctx.request_id.get(None) or "n/a"
|
||||
except AttributeError:
|
||||
record.request_id = "n/a"
|
||||
return True
|
||||
|
||||
@@ -49,8 +49,7 @@ app = Sanic(__name__, log_config=LOG_SETTINGS)
|
||||
|
||||
@app.on_request
|
||||
async def set_request_id(request):
|
||||
request_id = request.id
|
||||
context.set("X-Request-ID", request_id)
|
||||
request.app.ctx.request_id.set(request.id)
|
||||
log.info(f"Setting {request.id=}")
|
||||
|
||||
|
||||
@@ -61,14 +60,14 @@ async def set_request_header(request, response):
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
log.debug("X-Request-ID: %s", context.get("X-Request-ID"))
|
||||
log.debug("X-Request-ID: %s", request.id)
|
||||
log.info("Hello from test!")
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.before_server_start
|
||||
def setup(app, loop):
|
||||
loop.set_task_factory(context.task_factory)
|
||||
app.ctx.request_id = ContextVar("request_id")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from os import getenv
|
||||
from platform import node
|
||||
from uuid import getnode as get_mac
|
||||
@@ -7,10 +8,11 @@ from uuid import getnode as get_mac
|
||||
from logdna import LogDNAHandler
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.request import Request
|
||||
from sanic.response import json
|
||||
|
||||
log = logging.getLogger('logdna')
|
||||
|
||||
log = logging.getLogger("logdna")
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
|
||||
@@ -30,10 +32,12 @@ logdna_options = {
|
||||
"index_meta": True,
|
||||
"hostname": node(),
|
||||
"ip": get_my_ip_address(),
|
||||
"mac": get_mac_address()
|
||||
"mac": get_mac_address(),
|
||||
}
|
||||
|
||||
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
|
||||
logdna_handler = LogDNAHandler(
|
||||
getenv("LOGDNA_API_KEY"), options=logdna_options
|
||||
)
|
||||
|
||||
logdna = logging.getLogger(__name__)
|
||||
logdna.setLevel(logging.INFO)
|
||||
@@ -49,13 +53,8 @@ def log_request(request: Request):
|
||||
|
||||
@app.route("/")
|
||||
def default(request):
|
||||
return json({
|
||||
"response": "I was here"
|
||||
})
|
||||
return json({"response": "I was here"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=getenv("PORT", 8080)
|
||||
)
|
||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
||||
|
||||
@@ -59,31 +59,31 @@ async def handler_stream(request):
|
||||
return response.stream(body)
|
||||
|
||||
|
||||
@app.listener("before_server_start")
|
||||
@app.before_server_start
|
||||
async def listener_before_server_start(*args, **kwargs):
|
||||
print("before_server_start")
|
||||
|
||||
|
||||
@app.listener("after_server_start")
|
||||
@app.after_server_start
|
||||
async def listener_after_server_start(*args, **kwargs):
|
||||
print("after_server_start")
|
||||
|
||||
|
||||
@app.listener("before_server_stop")
|
||||
@app.before_server_stop
|
||||
async def listener_before_server_stop(*args, **kwargs):
|
||||
print("before_server_stop")
|
||||
|
||||
|
||||
@app.listener("after_server_stop")
|
||||
@app.after_server_stop
|
||||
async def listener_after_server_stop(*args, **kwargs):
|
||||
print("after_server_stop")
|
||||
|
||||
|
||||
@app.middleware("request")
|
||||
@app.on_request
|
||||
async def print_on_request(request):
|
||||
print("print_on_request")
|
||||
|
||||
|
||||
@app.middleware("response")
|
||||
@app.on_response
|
||||
async def print_on_response(request, response):
|
||||
print("print_on_response")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from signal import signal, SIGINT
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@@ -11,12 +12,19 @@ app = Sanic(__name__)
|
||||
async def test(request):
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
try:
|
||||
loop.run_forever()
|
||||
except:
|
||||
loop.stop()
|
||||
|
||||
async def main():
|
||||
server = await app.create_server(
|
||||
port=8000, host="0.0.0.0", return_asyncio_server=True
|
||||
)
|
||||
|
||||
if server is None:
|
||||
return
|
||||
|
||||
await server.startup()
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -1,32 +1,62 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from signal import signal, SIGINT
|
||||
import asyncio
|
||||
|
||||
from signal import SIGINT, signal
|
||||
|
||||
import uvloop
|
||||
|
||||
from sanic import Sanic, response
|
||||
from sanic.server import AsyncioServer
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def after_start_test(app, loop):
|
||||
print("Async Server Started!")
|
||||
|
||||
@app.before_server_start
|
||||
async def before_server_start(app, loop):
|
||||
print("Async Server starting")
|
||||
|
||||
|
||||
@app.after_server_start
|
||||
async def after_server_start(app, loop):
|
||||
print("Async Server started")
|
||||
|
||||
|
||||
@app.before_server_stop
|
||||
async def before_server_stop(app, loop):
|
||||
print("Async Server stopping")
|
||||
|
||||
|
||||
@app.after_server_stop
|
||||
async def after_server_stop(app, loop):
|
||||
print("Async Server stopped")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||
serv_coro = app.create_server(
|
||||
host="0.0.0.0", port=8000, return_asyncio_server=True
|
||||
)
|
||||
loop = asyncio.get_event_loop()
|
||||
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
server = loop.run_until_complete(serv_task)
|
||||
server.after_start()
|
||||
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 as e:
|
||||
except KeyboardInterrupt:
|
||||
loop.stop()
|
||||
finally:
|
||||
server.before_stop()
|
||||
loop.run_until_complete(server.before_stop())
|
||||
|
||||
# Wait for server to close
|
||||
close_task = server.close()
|
||||
@@ -35,4 +65,4 @@ finally:
|
||||
# Complete all tasks on the loop
|
||||
for connection in server.connections:
|
||||
connection.close_if_idle()
|
||||
server.after_stop()
|
||||
loop.run_until_complete(server.after_stop())
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
app = Sanic("some_name")
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
return text("I am get method")
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
return text("I am post method")
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
return text("I am put method")
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
return text("I am patch method")
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
return text("I am delete method")
|
||||
|
||||
|
||||
class SimpleAsyncView(HTTPMethodView):
|
||||
|
||||
async def get(self, request):
|
||||
return text('I am async get method')
|
||||
return text("I am async get method")
|
||||
|
||||
async def post(self, request):
|
||||
return text('I am async post method')
|
||||
return text("I am async post method")
|
||||
|
||||
async def put(self, request):
|
||||
return text('I am async put method')
|
||||
return text("I am async put method")
|
||||
|
||||
|
||||
app.add_route(SimpleView.as_view(), '/')
|
||||
app.add_route(SimpleAsyncView.as_view(), '/async')
|
||||
app.add_route(SimpleView.as_view(), "/")
|
||||
app.add_route(SimpleAsyncView.as_view(), "/async")
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import os
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.log import logger as log
|
||||
from sanic import response
|
||||
from sanic import Sanic, response
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.log import logger as log
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@@ -13,7 +13,7 @@ async def test_async(request):
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.route("/sync", methods=['GET', 'POST'])
|
||||
@app.route("/sync", methods=["GET", "POST"])
|
||||
def test_sync(request):
|
||||
return response.json({"test": True})
|
||||
|
||||
@@ -31,6 +31,7 @@ def exception(request):
|
||||
@app.route("/await")
|
||||
async def test_await(request):
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(5)
|
||||
return response.text("I'm feeling sleepy")
|
||||
|
||||
@@ -42,8 +43,10 @@ async def test_file(request):
|
||||
|
||||
@app.route("/file_stream")
|
||||
async def test_file_stream(request):
|
||||
return await response.file_stream(os.path.abspath("setup.py"),
|
||||
chunk_size=1024)
|
||||
return await response.file_stream(
|
||||
os.path.abspath("setup.py"), chunk_size=1024
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
# Exceptions
|
||||
@@ -52,14 +55,17 @@ async def test_file_stream(request):
|
||||
|
||||
@app.exception(ServerError)
|
||||
async def test(request, exception):
|
||||
return response.json({"exception": "{}".format(exception), "status": exception.status_code},
|
||||
status=exception.status_code)
|
||||
return response.json(
|
||||
{"exception": str(exception), "status": exception.status_code},
|
||||
status=exception.status_code,
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
# Read from request
|
||||
# ----------------------------------------------- #
|
||||
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return response.json({"received": True, "message": request.json})
|
||||
@@ -67,38 +73,51 @@ def post_json(request):
|
||||
|
||||
@app.route("/form")
|
||||
def post_form_json(request):
|
||||
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
|
||||
return response.json(
|
||||
{
|
||||
"received": True,
|
||||
"form_data": request.form,
|
||||
"test": request.form.get("test"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return response.json({"parsed": True, "args": request.args, "url": request.url,
|
||||
"query_string": request.query_string})
|
||||
return response.json(
|
||||
{
|
||||
"parsed": True,
|
||||
"args": request.args,
|
||||
"url": request.url,
|
||||
"query_string": request.query_string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
# Run Server
|
||||
# ----------------------------------------------- #
|
||||
|
||||
@app.listener('before_server_start')
|
||||
|
||||
@app.before_server_start
|
||||
def before_start(app, loop):
|
||||
log.info("SERVER STARTING")
|
||||
|
||||
|
||||
@app.listener('after_server_start')
|
||||
@app.after_server_start
|
||||
def after_start(app, loop):
|
||||
log.info("OH OH OH OH OHHHHHHHH")
|
||||
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
@app.before_server_stop
|
||||
def before_stop(app, loop):
|
||||
log.info("SERVER STOPPING")
|
||||
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
@app.after_server_stop
|
||||
def after_stop(app, loop):
|
||||
log.info("TRIED EVERYTHING")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
import socket
|
||||
import os
|
||||
import socket
|
||||
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@@ -10,14 +11,15 @@ app = Sanic(__name__)
|
||||
async def test(request):
|
||||
return response.text("OK")
|
||||
|
||||
if __name__ == '__main__':
|
||||
server_address = './uds_socket'
|
||||
|
||||
if __name__ == "__main__":
|
||||
server_address = "./uds_socket"
|
||||
# Make sure the socket does not already exist
|
||||
try:
|
||||
os.unlink(server_address)
|
||||
os.unlink(server_address)
|
||||
except OSError:
|
||||
if os.path.exists(server_address):
|
||||
raise
|
||||
if os.path.exists(server_address):
|
||||
raise
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.bind(server_address)
|
||||
app.run(sock=sock)
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
# generate a URL for the endpoint `post_handler`
|
||||
url = app.url_for('post_handler', post_id=5)
|
||||
url = app.url_for("post_handler", post_id=5)
|
||||
# the URL is `/posts/5`, redirect to it
|
||||
return response.redirect(url)
|
||||
|
||||
|
||||
@app.route('/posts/<post_id>')
|
||||
@app.route("/posts/<post_id>")
|
||||
async def post_handler(request, post_id):
|
||||
return response.text('Post - {}'.format(post_id))
|
||||
|
||||
if __name__ == '__main__':
|
||||
return response.text("Post - {}".format(post_id))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -8,7 +8,9 @@ app = Sanic(name="blue-print-group-version-example")
|
||||
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
|
||||
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
|
||||
|
||||
bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
|
||||
bpg = Blueprint.group(
|
||||
bp1, bp2, url_prefix="/sentient/robot", version=1, strict_slashes=True
|
||||
)
|
||||
|
||||
|
||||
@bp1.get("/name")
|
||||
@@ -31,5 +33,5 @@ async def bp2_revised_name(request):
|
||||
|
||||
app.blueprint(bpg)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import file
|
||||
from sanic.response import redirect
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
return await file('websocket.html')
|
||||
app.static("index.html", "websocket.html")
|
||||
|
||||
|
||||
@app.websocket('/feed')
|
||||
@app.route("/")
|
||||
def index(request):
|
||||
return redirect("index.html")
|
||||
|
||||
|
||||
@app.websocket("/feed")
|
||||
async def feed(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
data = "hello!"
|
||||
print("Sending: " + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
print("Received: " + data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
|
||||
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic.__version__ import __version__
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.constants import HTTPMethod
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, html, json, text
|
||||
|
||||
@@ -9,6 +10,7 @@ __all__ = (
|
||||
"__version__",
|
||||
"Sanic",
|
||||
"Blueprint",
|
||||
"HTTPMethod",
|
||||
"HTTPResponse",
|
||||
"Request",
|
||||
"html",
|
||||
|
||||
@@ -1,131 +1,15 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||
from importlib import import_module
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sanic import __version__
|
||||
from sanic.app import Sanic
|
||||
from sanic.config import BASE_LOGO
|
||||
from sanic.log import logger
|
||||
from sanic.cli.app import SanicCLI
|
||||
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||
|
||||
|
||||
class SanicArgumentParser(ArgumentParser):
|
||||
def add_bool_arguments(self, *args, **kwargs):
|
||||
group = self.add_mutually_exclusive_group()
|
||||
group.add_argument(*args, action="store_true", **kwargs)
|
||||
kwargs["help"] = "no " + kwargs["help"]
|
||||
group.add_argument(
|
||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||
)
|
||||
if OS_IS_WINDOWS:
|
||||
enable_windows_color_support()
|
||||
|
||||
|
||||
def main():
|
||||
parser = SanicArgumentParser(
|
||||
prog="sanic",
|
||||
description=BASE_LOGO,
|
||||
formatter_class=RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-H",
|
||||
"--host",
|
||||
dest="host",
|
||||
type=str,
|
||||
default="127.0.0.1",
|
||||
help="host address [default 127.0.0.1]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--port",
|
||||
dest="port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="port to serve on [default 8000]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--unix",
|
||||
dest="unix",
|
||||
type=str,
|
||||
default="",
|
||||
help="location of unix socket",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--workers",
|
||||
dest="workers",
|
||||
type=int,
|
||||
default=1,
|
||||
help="number of worker processes [default 1]",
|
||||
)
|
||||
parser.add_argument("--debug", dest="debug", action="store_true")
|
||||
parser.add_bool_arguments(
|
||||
"--access-logs", dest="access_log", help="display access logs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"Sanic {__version__}",
|
||||
)
|
||||
parser.add_argument(
|
||||
"module", help="path to your Sanic app. Example: path.to.server:app"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
module_path = os.path.abspath(os.getcwd())
|
||||
if module_path not in sys.path:
|
||||
sys.path.append(module_path)
|
||||
|
||||
if ":" in args.module:
|
||||
module_name, app_name = args.module.rsplit(":", 1)
|
||||
else:
|
||||
module_parts = args.module.split(".")
|
||||
module_name = ".".join(module_parts[:-1])
|
||||
app_name = module_parts[-1]
|
||||
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
app_name = type(app).__name__
|
||||
|
||||
if not isinstance(app, Sanic):
|
||||
raise ValueError(
|
||||
f"Module is not a Sanic app, it is a {app_name}. "
|
||||
f"Perhaps you meant {args.module}.app?"
|
||||
)
|
||||
if args.cert is not None or args.key is not None:
|
||||
ssl = {
|
||||
"cert": args.cert,
|
||||
"key": args.key,
|
||||
} # type: Optional[Dict[str, Any]]
|
||||
else:
|
||||
ssl = None
|
||||
|
||||
app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
unix=args.unix,
|
||||
workers=args.workers,
|
||||
debug=args.debug,
|
||||
access_log=args.access_log,
|
||||
ssl=ssl,
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error(
|
||||
f"No module named {e.name} found.\n"
|
||||
f" Example File: project/sanic_server.py -> app\n"
|
||||
f" Example Module: project.sanic_server.app"
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception("Failed to run app")
|
||||
cli = SanicCLI()
|
||||
cli.attach()
|
||||
cli.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "21.3.2"
|
||||
__version__ = "21.12.0dev"
|
||||
|
||||
895
sanic/app.py
895
sanic/app.py
File diff suppressed because it is too large
Load Diff
0
sanic/application/__init__.py
Normal file
0
sanic/application/__init__.py
Normal file
57
sanic/application/logo.py
Normal file
57
sanic/application/logo.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
from os import environ
|
||||
|
||||
|
||||
BASE_LOGO = """
|
||||
|
||||
Sanic
|
||||
Build Fast. Run Fast.
|
||||
|
||||
"""
|
||||
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▄████████▄ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ██ ██▀▀▄ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ███████████ █ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ███████████▄▄▀ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▀███████▀ \033[0m
|
||||
\033[48;2;255;13;104m \033[0m
|
||||
Dark roast. No sugar."""
|
||||
|
||||
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▄███ █████ ██ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▀███████ ███▄ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ████ ████████▀ \033[0m
|
||||
\033[48;2;255;13;104m \033[0m
|
||||
Build Fast. Run Fast."""
|
||||
|
||||
FULL_COLOR_LOGO = """
|
||||
|
||||
\033[38;2;255;13;104m ▄███ █████ ██ \033[0m ▄█▄ ██ █ █ ▄██████████
|
||||
\033[38;2;255;13;104m ██ \033[0m █ █ █ ██ █ █ ██
|
||||
\033[38;2;255;13;104m ▀███████ ███▄ \033[0m ▀ █ █ ██ ▄ █ ██
|
||||
\033[38;2;255;13;104m ██\033[0m █████████ █ ██ █ █ ▄▄
|
||||
\033[38;2;255;13;104m ████ ████████▀ \033[0m █ █ █ ██ █ ▀██ ███████
|
||||
|
||||
""" # noqa
|
||||
|
||||
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||
|
||||
|
||||
def get_logo(full=False, coffee=False):
|
||||
logo = (
|
||||
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
|
||||
if sys.stdout.isatty()
|
||||
else BASE_LOGO
|
||||
)
|
||||
|
||||
if (
|
||||
sys.platform == "darwin"
|
||||
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
|
||||
):
|
||||
logo = ansi_pattern.sub("", logo)
|
||||
|
||||
return logo
|
||||
146
sanic/application/motd.py
Normal file
146
sanic/application/motd.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import sys
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from shutil import get_terminal_size
|
||||
from textwrap import indent, wrap
|
||||
from typing import Dict, Optional
|
||||
|
||||
from sanic import __version__
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
class MOTD(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
logo: Optional[str],
|
||||
serve_location: str,
|
||||
data: Dict[str, str],
|
||||
extra: Dict[str, str],
|
||||
) -> None:
|
||||
self.logo = logo
|
||||
self.serve_location = serve_location
|
||||
self.data = data
|
||||
self.extra = extra
|
||||
self.key_width = 0
|
||||
self.value_width = 0
|
||||
|
||||
@abstractmethod
|
||||
def display(self):
|
||||
... # noqa
|
||||
|
||||
@classmethod
|
||||
def output(
|
||||
cls,
|
||||
logo: Optional[str],
|
||||
serve_location: str,
|
||||
data: Dict[str, str],
|
||||
extra: Dict[str, str],
|
||||
) -> None:
|
||||
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
|
||||
motd_class(logo, serve_location, data, extra).display()
|
||||
|
||||
|
||||
class MOTDBasic(MOTD):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def display(self):
|
||||
if self.logo:
|
||||
logger.debug(self.logo)
|
||||
lines = [f"Sanic v{__version__}"]
|
||||
if self.serve_location:
|
||||
lines.append(f"Goin' Fast @ {self.serve_location}")
|
||||
lines += [
|
||||
*(f"{key}: {value}" for key, value in self.data.items()),
|
||||
*(f"{key}: {value}" for key, value in self.extra.items()),
|
||||
]
|
||||
for line in lines:
|
||||
logger.info(line)
|
||||
|
||||
|
||||
class MOTDTTY(MOTD):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_variables()
|
||||
|
||||
def set_variables(self): # no cov
|
||||
fallback = (108, 24)
|
||||
terminal_width = max(
|
||||
get_terminal_size(fallback=fallback).columns, fallback[0]
|
||||
)
|
||||
self.max_value_width = terminal_width - fallback[0] + 36
|
||||
|
||||
self.key_width = 4
|
||||
self.value_width = self.max_value_width
|
||||
if self.data:
|
||||
self.key_width = max(map(len, self.data.keys()))
|
||||
self.value_width = min(
|
||||
max(map(len, self.data.values())), self.max_value_width
|
||||
)
|
||||
self.logo_lines = self.logo.split("\n") if self.logo else []
|
||||
self.logo_line_length = 24
|
||||
self.centering_length = (
|
||||
self.key_width + self.value_width + 2 + self.logo_line_length
|
||||
)
|
||||
self.display_length = self.key_width + self.value_width + 2
|
||||
|
||||
def display(self):
|
||||
version = f"Sanic v{__version__}".center(self.centering_length)
|
||||
running = (
|
||||
f"Goin' Fast @ {self.serve_location}"
|
||||
if self.serve_location
|
||||
else ""
|
||||
).center(self.centering_length)
|
||||
length = len(version) + 2 - self.logo_line_length
|
||||
first_filler = "─" * (self.logo_line_length - 1)
|
||||
second_filler = "─" * length
|
||||
display_filler = "─" * (self.display_length + 2)
|
||||
lines = [
|
||||
f"\n┌{first_filler}─{second_filler}┐",
|
||||
f"│ {version} │",
|
||||
f"│ {running} │",
|
||||
f"├{first_filler}┬{second_filler}┤",
|
||||
]
|
||||
|
||||
self._render_data(lines, self.data, 0)
|
||||
if self.extra:
|
||||
logo_part = self._get_logo_part(len(lines) - 4)
|
||||
lines.append(f"| {logo_part} ├{display_filler}┤")
|
||||
self._render_data(lines, self.extra, len(lines) - 4)
|
||||
|
||||
self._render_fill(lines)
|
||||
|
||||
lines.append(f"└{first_filler}┴{second_filler}┘\n")
|
||||
logger.info(indent("\n".join(lines), " "))
|
||||
|
||||
def _render_data(self, lines, data, start):
|
||||
offset = 0
|
||||
for idx, (key, value) in enumerate(data.items(), start=start):
|
||||
key = key.rjust(self.key_width)
|
||||
|
||||
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
|
||||
for wrap_index, part in enumerate(wrapped):
|
||||
part = part.ljust(self.value_width)
|
||||
logo_part = self._get_logo_part(idx + offset + wrap_index)
|
||||
display = (
|
||||
f"{key}: {part}"
|
||||
if wrap_index == 0
|
||||
else (" " * len(key) + f" {part}")
|
||||
)
|
||||
lines.append(f"│ {logo_part} │ {display} │")
|
||||
if wrap_index:
|
||||
offset += 1
|
||||
|
||||
def _render_fill(self, lines):
|
||||
filler = " " * self.display_length
|
||||
idx = len(lines) - 5
|
||||
for i in range(1, len(self.logo_lines) - idx):
|
||||
logo_part = self.logo_lines[idx + i]
|
||||
lines.append(f"│ {logo_part} │ {filler} │")
|
||||
|
||||
def _get_logo_part(self, idx):
|
||||
try:
|
||||
logo_part = self.logo_lines[idx]
|
||||
except IndexError:
|
||||
logo_part = " " * (self.logo_line_length - 3)
|
||||
return logo_part
|
||||
74
sanic/application/state.py
Normal file
74
sanic/application/state.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Set, Union
|
||||
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
class StrEnum(str, Enum):
|
||||
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
||||
return name.lower()
|
||||
|
||||
|
||||
class Server(StrEnum):
|
||||
SANIC = auto()
|
||||
ASGI = auto()
|
||||
GUNICORN = auto()
|
||||
|
||||
|
||||
class Mode(StrEnum):
|
||||
PRODUCTION = auto()
|
||||
DEBUG = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationState:
|
||||
app: Sanic
|
||||
asgi: bool = field(default=False)
|
||||
coffee: bool = field(default=False)
|
||||
fast: bool = field(default=False)
|
||||
host: str = field(default="")
|
||||
mode: Mode = field(default=Mode.PRODUCTION)
|
||||
port: int = field(default=0)
|
||||
reload_dirs: Set[Path] = field(default_factory=set)
|
||||
server: Server = field(default=Server.SANIC)
|
||||
is_running: bool = field(default=False)
|
||||
is_started: bool = field(default=False)
|
||||
is_stopping: bool = field(default=False)
|
||||
verbosity: int = field(default=0)
|
||||
workers: int = field(default=0)
|
||||
|
||||
# This property relates to the ApplicationState instance and should
|
||||
# not be changed except in the __post_init__ method
|
||||
_init: bool = field(default=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._init = True
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if self._init and name == "_init":
|
||||
raise RuntimeError(
|
||||
"Cannot change the value of _init after instantiation"
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
if self._init and hasattr(self, f"set_{name}"):
|
||||
getattr(self, f"set_{name}")(value)
|
||||
|
||||
def set_mode(self, value: Union[str, Mode]):
|
||||
if hasattr(self.app, "error_handler"):
|
||||
self.app.error_handler.debug = self.app.debug
|
||||
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
@property
|
||||
def is_debug(self):
|
||||
return self.mode is Mode.DEBUG
|
||||
@@ -1,6 +1,5 @@
|
||||
import warnings
|
||||
|
||||
from inspect import isawaitable
|
||||
from typing import Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
@@ -8,24 +7,32 @@ import sanic.app # noqa
|
||||
|
||||
from sanic.compat import Header
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.http import Stage
|
||||
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse
|
||||
from sanic.server import ConnInfo
|
||||
from sanic.websocket import WebSocketConnection
|
||||
from sanic.server.websockets.connection import WebSocketConnection
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self, asgi_app: "ASGIApp") -> None:
|
||||
self.asgi_app = asgi_app
|
||||
|
||||
if "before_server_start" in self.asgi_app.sanic_app.listeners:
|
||||
if (
|
||||
"server.init.before"
|
||||
in self.asgi_app.sanic_app.signal_router.name_index
|
||||
):
|
||||
warnings.warn(
|
||||
'You have set a listener for "before_server_start" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as early as possible, but not before "
|
||||
"the ASGI server is started."
|
||||
)
|
||||
if "after_server_stop" in self.asgi_app.sanic_app.listeners:
|
||||
if (
|
||||
"server.shutdown.after"
|
||||
in self.asgi_app.sanic_app.signal_router.name_index
|
||||
):
|
||||
warnings.warn(
|
||||
'You have set a listener for "after_server_stop" '
|
||||
"in ASGI mode. "
|
||||
@@ -42,19 +49,9 @@ class Lifespan:
|
||||
in sequence since the ASGI lifespan protocol only supports a single
|
||||
startup event.
|
||||
"""
|
||||
self.asgi_app.sanic_app.router.finalize()
|
||||
if self.asgi_app.sanic_app.signal_router.routes:
|
||||
self.asgi_app.sanic_app.signal_router.finalize()
|
||||
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||
"before_server_start", []
|
||||
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
|
||||
|
||||
for handler in listeners:
|
||||
response = handler(
|
||||
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||
)
|
||||
if response and isawaitable(response):
|
||||
await response
|
||||
await self.asgi_app.sanic_app._startup()
|
||||
await self.asgi_app.sanic_app._server_event("init", "before")
|
||||
await self.asgi_app.sanic_app._server_event("init", "after")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
@@ -65,16 +62,8 @@ class Lifespan:
|
||||
in sequence since the ASGI lifespan protocol only supports a single
|
||||
shutdown event.
|
||||
"""
|
||||
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||
"before_server_stop", []
|
||||
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
|
||||
|
||||
for handler in listeners:
|
||||
response = handler(
|
||||
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||
)
|
||||
if response and isawaitable(response):
|
||||
await response
|
||||
await self.asgi_app.sanic_app._server_event("shutdown", "before")
|
||||
await self.asgi_app.sanic_app._server_event("shutdown", "after")
|
||||
|
||||
async def __call__(
|
||||
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||
@@ -96,6 +85,8 @@ class ASGIApp:
|
||||
transport: MockTransport
|
||||
lifespan: Lifespan
|
||||
ws: Optional[WebSocketConnection]
|
||||
stage: Stage
|
||||
response: Optional[BaseHTTPResponse]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.ws = None
|
||||
@@ -108,6 +99,8 @@ class ASGIApp:
|
||||
instance.sanic_app = sanic_app
|
||||
instance.transport = MockTransport(scope, receive, send)
|
||||
instance.transport.loop = sanic_app.loop
|
||||
instance.stage = Stage.IDLE
|
||||
instance.response = None
|
||||
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
|
||||
|
||||
headers = Header(
|
||||
@@ -140,7 +133,6 @@ class ASGIApp:
|
||||
instance.ws = instance.transport.create_websocket_connection(
|
||||
send, receive
|
||||
)
|
||||
await instance.ws.accept()
|
||||
else:
|
||||
raise ServerError("Received unknown ASGI scope")
|
||||
|
||||
@@ -163,11 +155,15 @@ class ASGIApp:
|
||||
"""
|
||||
Read and stream the body in chunks from an incoming ASGI message.
|
||||
"""
|
||||
if self.stage is Stage.IDLE:
|
||||
self.stage = Stage.REQUEST
|
||||
message = await self.transport.receive()
|
||||
body = message.get("body", b"")
|
||||
if not message.get("more_body", False):
|
||||
self.request_body = False
|
||||
return None
|
||||
return message.get("body", b"")
|
||||
if not body:
|
||||
return None
|
||||
return body
|
||||
|
||||
async def __aiter__(self):
|
||||
while self.request_body:
|
||||
@@ -175,11 +171,17 @@ class ASGIApp:
|
||||
if data:
|
||||
yield data
|
||||
|
||||
def respond(self, response):
|
||||
def respond(self, response: BaseHTTPResponse):
|
||||
if self.stage is not Stage.HANDLER:
|
||||
self.stage = Stage.FAILED
|
||||
raise RuntimeError("Response already started")
|
||||
if self.response is not None:
|
||||
self.response.stream = None
|
||||
response.stream, self.response = self, response
|
||||
return response
|
||||
|
||||
async def send(self, data, end_stream):
|
||||
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
|
||||
if self.response:
|
||||
response, self.response = self.response, None
|
||||
await self.transport.send(
|
||||
@@ -206,4 +208,8 @@ class ASGIApp:
|
||||
"""
|
||||
Handle the incoming request.
|
||||
"""
|
||||
await self.sanic_app.handle_request(self.request)
|
||||
try:
|
||||
self.stage = Stage.HANDLER
|
||||
await self.sanic_app.handle_request(self.request)
|
||||
except Exception as e:
|
||||
await self.sanic_app.handle_exception(self.request, e)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import re
|
||||
|
||||
from typing import Any, Tuple
|
||||
from warnings import warn
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.mixins.exceptions import ExceptionMixin
|
||||
from sanic.mixins.listeners import ListenerMixin
|
||||
from sanic.mixins.middleware import MiddlewareMixin
|
||||
@@ -8,6 +11,9 @@ from sanic.mixins.routes import RouteMixin
|
||||
from sanic.mixins.signals import SignalMixin
|
||||
|
||||
|
||||
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
|
||||
|
||||
|
||||
class BaseSanic(
|
||||
RouteMixin,
|
||||
MiddlewareMixin,
|
||||
@@ -17,7 +23,25 @@ class BaseSanic(
|
||||
):
|
||||
__fake_slots__: Tuple[str, ...]
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
|
||||
class_name = self.__class__.__name__
|
||||
|
||||
if name is None:
|
||||
raise SanicException(
|
||||
f"{class_name} instance cannot be unnamed. "
|
||||
"Please use Sanic(name='your_application_name') instead.",
|
||||
)
|
||||
|
||||
if not VALID_NAME.match(name):
|
||||
warn(
|
||||
f"{class_name} instance named '{name}' uses a format that is"
|
||||
f"deprecated. Starting in version 21.12, {class_name} objects "
|
||||
"must be named only using alphanumeric characters, _, or -.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
self.name = name
|
||||
|
||||
for base in BaseSanic.__bases__:
|
||||
base.__init__(self, *args, **kwargs) # type: ignore
|
||||
|
||||
@@ -34,8 +58,9 @@ class BaseSanic(
|
||||
if name not in self.__fake_slots__:
|
||||
warn(
|
||||
f"Setting variables on {self.__class__.__name__} instances is "
|
||||
"deprecated and will be removed in version 21.9. You should "
|
||||
"deprecated and will be removed in version 21.12. You should "
|
||||
f"change your {self.__class__.__name__} instance to use "
|
||||
f"instance.ctx.{name} instead."
|
||||
f"instance.ctx.{name} instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
from collections.abc import MutableSequence
|
||||
from typing import List, Optional, Union
|
||||
from __future__ import annotations
|
||||
|
||||
import sanic
|
||||
from collections.abc import MutableSequence
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
class BlueprintGroup(MutableSequence):
|
||||
@@ -54,9 +59,21 @@ class BlueprintGroup(MutableSequence):
|
||||
app.blueprint(bpg)
|
||||
"""
|
||||
|
||||
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")
|
||||
__slots__ = (
|
||||
"_blueprints",
|
||||
"_url_prefix",
|
||||
"_version",
|
||||
"_strict_slashes",
|
||||
"_version_prefix",
|
||||
)
|
||||
|
||||
def __init__(self, url_prefix=None, version=None, strict_slashes=None):
|
||||
def __init__(
|
||||
self,
|
||||
url_prefix: Optional[str] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: str = "/v",
|
||||
):
|
||||
"""
|
||||
Create a new Blueprint Group
|
||||
|
||||
@@ -65,13 +82,14 @@ class BlueprintGroup(MutableSequence):
|
||||
inherited by each of the Blueprint
|
||||
:param strict_slashes: URL Strict slash behavior indicator
|
||||
"""
|
||||
self._blueprints = []
|
||||
self._blueprints: List[Blueprint] = []
|
||||
self._url_prefix = url_prefix
|
||||
self._version = version
|
||||
self._version_prefix = version_prefix
|
||||
self._strict_slashes = strict_slashes
|
||||
|
||||
@property
|
||||
def url_prefix(self) -> str:
|
||||
def url_prefix(self) -> Optional[Union[int, str, float]]:
|
||||
"""
|
||||
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||
|
||||
@@ -80,7 +98,7 @@ class BlueprintGroup(MutableSequence):
|
||||
return self._url_prefix
|
||||
|
||||
@property
|
||||
def blueprints(self) -> List["sanic.Blueprint"]:
|
||||
def blueprints(self) -> List[Blueprint]:
|
||||
"""
|
||||
Retrieve a list of all the available blueprints under this group.
|
||||
|
||||
@@ -107,6 +125,15 @@ class BlueprintGroup(MutableSequence):
|
||||
"""
|
||||
return self._strict_slashes
|
||||
|
||||
@property
|
||||
def version_prefix(self) -> str:
|
||||
"""
|
||||
Version prefix; defaults to ``/v``
|
||||
|
||||
:return: str
|
||||
"""
|
||||
return self._version_prefix
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Tun the class Blueprint Group into an Iterable item
|
||||
@@ -161,34 +188,37 @@ class BlueprintGroup(MutableSequence):
|
||||
"""
|
||||
return len(self._blueprints)
|
||||
|
||||
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
|
||||
"""
|
||||
Sanitize the Blueprint Entity to override the Version and strict slash
|
||||
behaviors as required.
|
||||
|
||||
:param bp: Sanic Blueprint entity Object
|
||||
:return: Modified Blueprint
|
||||
"""
|
||||
if self._url_prefix:
|
||||
merged_prefix = "/".join(
|
||||
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
|
||||
).rstrip("/")
|
||||
bp.url_prefix = f"/{merged_prefix}"
|
||||
for _attr in ["version", "strict_slashes"]:
|
||||
if getattr(bp, _attr) is None:
|
||||
setattr(bp, _attr, getattr(self, _attr))
|
||||
return bp
|
||||
|
||||
def append(self, value: "sanic.Blueprint") -> None:
|
||||
def append(self, value: Blueprint) -> None:
|
||||
"""
|
||||
The Abstract class `MutableSequence` leverages this append method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
:param value: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints.append(self._sanitize_blueprint(bp=value))
|
||||
self._blueprints.append(value)
|
||||
|
||||
def insert(self, index: int, item: "sanic.Blueprint") -> None:
|
||||
def exception(self, *exceptions, **kwargs):
|
||||
"""
|
||||
A decorator that can be used to implement a global exception handler
|
||||
for all the Blueprints that belong to this Blueprint Group.
|
||||
|
||||
In case of nested Blueprint Groups, the same handler is applied
|
||||
across each of the Blueprints recursively.
|
||||
|
||||
:param args: List of Python exceptions to be caught by the handler
|
||||
:param kwargs: Additional optional arguments to be passed to the
|
||||
exception handler
|
||||
:return: a decorated method to handle global exceptions for any
|
||||
blueprint registered under this group.
|
||||
"""
|
||||
|
||||
def register_exception_handler_for_blueprints(fn):
|
||||
for blueprint in self.blueprints:
|
||||
blueprint.exception(*exceptions, **kwargs)(fn)
|
||||
|
||||
return register_exception_handler_for_blueprints
|
||||
|
||||
def insert(self, index: int, item: Blueprint) -> None:
|
||||
"""
|
||||
The Abstract class `MutableSequence` leverages this insert method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
@@ -197,7 +227,7 @@ class BlueprintGroup(MutableSequence):
|
||||
:param item: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints.insert(index, self._sanitize_blueprint(item))
|
||||
self._blueprints.insert(index, item)
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
@@ -221,3 +251,15 @@ class BlueprintGroup(MutableSequence):
|
||||
args = list(args)[1:]
|
||||
return register_middleware_for_blueprints(fn)
|
||||
return register_middleware_for_blueprints
|
||||
|
||||
def on_request(self, middleware=None):
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "request")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="request")
|
||||
|
||||
def on_response(self, middleware=None):
|
||||
if callable(middleware):
|
||||
return self.middleware(middleware, "response")
|
||||
else:
|
||||
return partial(self.middleware, attach_to="response")
|
||||
|
||||
@@ -3,8 +3,23 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from functools import wraps
|
||||
from inspect import isfunction
|
||||
from itertools import chain
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from sanic_routing.exceptions import NotFound # type: ignore
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
@@ -12,6 +27,7 @@ from sanic_routing.route import Route # type: ignore
|
||||
from sanic.base import BaseSanic
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.helpers import Default, _default
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
from sanic.models.handler_types import (
|
||||
ListenerType,
|
||||
@@ -24,6 +40,32 @@ if TYPE_CHECKING:
|
||||
from sanic import Sanic # noqa
|
||||
|
||||
|
||||
def lazy(func, as_decorator=True):
|
||||
@wraps(func)
|
||||
def decorator(bp, *args, **kwargs):
|
||||
nonlocal as_decorator
|
||||
kwargs["apply"] = False
|
||||
pass_handler = None
|
||||
|
||||
if args and isfunction(args[0]):
|
||||
as_decorator = False
|
||||
|
||||
def wrapper(handler):
|
||||
future = func(bp, *args, **kwargs)
|
||||
if as_decorator:
|
||||
future = future(handler)
|
||||
|
||||
if bp.registered:
|
||||
for app in bp.apps:
|
||||
bp.register(app, {})
|
||||
|
||||
return future
|
||||
|
||||
return wrapper if as_decorator else wrapper(pass_handler)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class Blueprint(BaseSanic):
|
||||
"""
|
||||
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
||||
@@ -37,10 +79,10 @@ class Blueprint(BaseSanic):
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param host: IP Address of FQDN for the sanic server to use.
|
||||
:param host: IP Address or FQDN for the sanic server to use.
|
||||
:param version: Blueprint Version
|
||||
:param strict_slashes: Enforce the API urls are requested with a
|
||||
training */*
|
||||
trailing */*
|
||||
"""
|
||||
|
||||
__fake_slots__ = (
|
||||
@@ -62,28 +104,24 @@ class Blueprint(BaseSanic):
|
||||
"strict_slashes",
|
||||
"url_prefix",
|
||||
"version",
|
||||
"version_prefix",
|
||||
"websocket_routes",
|
||||
)
|
||||
__pre_registry__: Dict[Union[str, Default], Any] = defaultdict(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
name: str = None,
|
||||
url_prefix: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
host: Optional[Union[List[str], str]] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: str = "/v",
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self._apps: Set[Sanic] = set()
|
||||
super().__init__(name=name)
|
||||
self.reset()
|
||||
self.ctx = SimpleNamespace()
|
||||
self.exceptions: List[RouteHandler] = []
|
||||
self.host = host
|
||||
self.listeners: Dict[str, List[ListenerType]] = {}
|
||||
self.middlewares: List[MiddlewareType] = []
|
||||
self.name = name
|
||||
self.routes: List[Route] = []
|
||||
self.statics: List[RouteHandler] = []
|
||||
self.strict_slashes = strict_slashes
|
||||
self.url_prefix = (
|
||||
url_prefix[:-1]
|
||||
@@ -91,7 +129,7 @@ class Blueprint(BaseSanic):
|
||||
else url_prefix
|
||||
)
|
||||
self.version = version
|
||||
self.websocket_routes: List[Route] = []
|
||||
self.version_prefix = version_prefix
|
||||
|
||||
def __repr__(self) -> str:
|
||||
args = ", ".join(
|
||||
@@ -118,32 +156,100 @@ class Blueprint(BaseSanic):
|
||||
)
|
||||
return self._apps
|
||||
|
||||
def route(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().route(*args, **kwargs)
|
||||
@property
|
||||
def registered(self) -> bool:
|
||||
return bool(self._apps)
|
||||
|
||||
def static(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().static(*args, **kwargs)
|
||||
exception = lazy(BaseSanic.exception)
|
||||
listener = lazy(BaseSanic.listener)
|
||||
middleware = lazy(BaseSanic.middleware)
|
||||
route = lazy(BaseSanic.route)
|
||||
signal = lazy(BaseSanic.signal)
|
||||
static = lazy(BaseSanic.static, as_decorator=False)
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().middleware(*args, **kwargs)
|
||||
def reset(self):
|
||||
self._apps: Set[Sanic] = set()
|
||||
self.exceptions: List[RouteHandler] = []
|
||||
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
|
||||
self.middlewares: List[MiddlewareType] = []
|
||||
self.routes: List[Route] = []
|
||||
self.statics: List[RouteHandler] = []
|
||||
self.websocket_routes: List[Route] = []
|
||||
|
||||
def listener(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().listener(*args, **kwargs)
|
||||
def copy(
|
||||
self,
|
||||
name: str,
|
||||
url_prefix: Optional[Union[str, Default]] = _default,
|
||||
version: Optional[Union[int, str, float, Default]] = _default,
|
||||
version_prefix: Union[str, Default] = _default,
|
||||
strict_slashes: Optional[Union[bool, Default]] = _default,
|
||||
with_registration: bool = True,
|
||||
with_ctx: bool = False,
|
||||
):
|
||||
"""
|
||||
Copy a blueprint instance with some optional parameters to
|
||||
override the values of attributes in the old instance.
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().exception(*args, **kwargs)
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param version: Blueprint Version
|
||||
:param version_prefix: the prefix of the version number shown in the
|
||||
URL.
|
||||
:param strict_slashes: Enforce the API urls are requested with a
|
||||
trailing */*
|
||||
:param with_registration: whether register new blueprint instance with
|
||||
sanic apps that were registered with the old instance or not.
|
||||
:param with_ctx: whether ``ctx`` will be copied or not.
|
||||
"""
|
||||
|
||||
def signal(self, event: str, *args, **kwargs):
|
||||
kwargs["apply"] = False
|
||||
return super().signal(event, *args, **kwargs)
|
||||
attrs_backup = {
|
||||
"_apps": self._apps,
|
||||
"routes": self.routes,
|
||||
"websocket_routes": self.websocket_routes,
|
||||
"middlewares": self.middlewares,
|
||||
"exceptions": self.exceptions,
|
||||
"listeners": self.listeners,
|
||||
"statics": self.statics,
|
||||
}
|
||||
|
||||
self.reset()
|
||||
new_bp = deepcopy(self)
|
||||
new_bp.name = name
|
||||
|
||||
if not isinstance(url_prefix, Default):
|
||||
new_bp.url_prefix = url_prefix
|
||||
if not isinstance(version, Default):
|
||||
new_bp.version = version
|
||||
if not isinstance(strict_slashes, Default):
|
||||
new_bp.strict_slashes = strict_slashes
|
||||
if not isinstance(version_prefix, Default):
|
||||
new_bp.version_prefix = version_prefix
|
||||
|
||||
for key, value in attrs_backup.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
if with_registration and self._apps:
|
||||
if new_bp._future_statics:
|
||||
raise SanicException(
|
||||
"Static routes registered with the old blueprint instance,"
|
||||
" cannot be registered again."
|
||||
)
|
||||
for app in self._apps:
|
||||
app.blueprint(new_bp)
|
||||
|
||||
if not with_ctx:
|
||||
new_bp.ctx = SimpleNamespace()
|
||||
|
||||
return new_bp
|
||||
|
||||
@staticmethod
|
||||
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
|
||||
def group(
|
||||
*blueprints: Union[Blueprint, BlueprintGroup],
|
||||
url_prefix: Optional[str] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: str = "/v",
|
||||
) -> BlueprintGroup:
|
||||
"""
|
||||
Create a list of blueprints, optionally grouping them under a
|
||||
general URL prefix.
|
||||
@@ -160,8 +266,6 @@ class Blueprint(BaseSanic):
|
||||
for i in nested:
|
||||
if isinstance(i, (list, tuple)):
|
||||
yield from chain(i)
|
||||
elif isinstance(i, BlueprintGroup):
|
||||
yield from i.blueprints
|
||||
else:
|
||||
yield i
|
||||
|
||||
@@ -169,6 +273,7 @@ class Blueprint(BaseSanic):
|
||||
url_prefix=url_prefix,
|
||||
version=version,
|
||||
strict_slashes=strict_slashes,
|
||||
version_prefix=version_prefix,
|
||||
)
|
||||
for bp in chain(blueprints):
|
||||
bps.append(bp)
|
||||
@@ -186,11 +291,18 @@ class Blueprint(BaseSanic):
|
||||
|
||||
self._apps.add(app)
|
||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
||||
opt_version = options.get("version", None)
|
||||
opt_strict_slashes = options.get("strict_slashes", None)
|
||||
opt_version_prefix = options.get("version_prefix", self.version_prefix)
|
||||
error_format = options.get(
|
||||
"error_format", app.config.FALLBACK_ERROR_FORMAT
|
||||
)
|
||||
|
||||
routes = []
|
||||
middleware = []
|
||||
exception_handlers = []
|
||||
listeners = defaultdict(list)
|
||||
registered = set()
|
||||
|
||||
# Routes
|
||||
for future in self._future_routes:
|
||||
@@ -200,30 +312,49 @@ class Blueprint(BaseSanic):
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
|
||||
strict_slashes = (
|
||||
self.strict_slashes
|
||||
if future.strict_slashes is None
|
||||
and self.strict_slashes is not None
|
||||
else future.strict_slashes
|
||||
version_prefix = self.version_prefix
|
||||
for prefix in (
|
||||
future.version_prefix,
|
||||
opt_version_prefix,
|
||||
):
|
||||
if prefix and prefix != "/v":
|
||||
version_prefix = prefix
|
||||
break
|
||||
|
||||
version = self._extract_value(
|
||||
future.version, opt_version, self.version
|
||||
)
|
||||
strict_slashes = self._extract_value(
|
||||
future.strict_slashes, opt_strict_slashes, self.strict_slashes
|
||||
)
|
||||
|
||||
name = app._generate_name(future.name)
|
||||
host = future.host or self.host
|
||||
if isinstance(host, list):
|
||||
host = tuple(host)
|
||||
|
||||
apply_route = FutureRoute(
|
||||
future.handler,
|
||||
uri[1:] if uri.startswith("//") else uri,
|
||||
future.methods,
|
||||
future.host or self.host,
|
||||
host,
|
||||
strict_slashes,
|
||||
future.stream,
|
||||
future.version or self.version,
|
||||
version,
|
||||
name,
|
||||
future.ignore_body,
|
||||
future.websocket,
|
||||
future.subprotocols,
|
||||
future.unquote,
|
||||
future.static,
|
||||
version_prefix,
|
||||
error_format,
|
||||
)
|
||||
|
||||
if (self, apply_route) in app._future_registry:
|
||||
continue
|
||||
|
||||
registered.add(apply_route)
|
||||
route = app._apply_route(apply_route)
|
||||
operation = (
|
||||
routes.extend if isinstance(route, list) else routes.append
|
||||
@@ -235,37 +366,64 @@ class Blueprint(BaseSanic):
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
apply_route = FutureStatic(uri, *future[1:])
|
||||
|
||||
if (self, apply_route) in app._future_registry:
|
||||
continue
|
||||
|
||||
registered.add(apply_route)
|
||||
route = app._apply_static(apply_route)
|
||||
routes.append(route)
|
||||
|
||||
route_names = [route.name for route in routes if route]
|
||||
|
||||
# Middleware
|
||||
if route_names:
|
||||
# Middleware
|
||||
for future in self._future_middleware:
|
||||
if (self, future) in app._future_registry:
|
||||
continue
|
||||
middleware.append(app._apply_middleware(future, route_names))
|
||||
|
||||
# Exceptions
|
||||
for future in self._future_exceptions:
|
||||
exception_handlers.append(app._apply_exception_handler(future))
|
||||
# Exceptions
|
||||
for future in self._future_exceptions:
|
||||
if (self, future) in app._future_registry:
|
||||
continue
|
||||
exception_handlers.append(
|
||||
app._apply_exception_handler(future, route_names)
|
||||
)
|
||||
|
||||
# Event listeners
|
||||
for listener in self._future_listeners:
|
||||
listeners[listener.event].append(app._apply_listener(listener))
|
||||
for future in self._future_listeners:
|
||||
if (self, future) in app._future_registry:
|
||||
continue
|
||||
listeners[future.event].append(app._apply_listener(future))
|
||||
|
||||
for signal in self._future_signals:
|
||||
signal.condition.update({"blueprint": self.name})
|
||||
app._apply_signal(signal)
|
||||
# Signals
|
||||
for future in self._future_signals:
|
||||
if (self, future) in app._future_registry:
|
||||
continue
|
||||
future.condition.update({"blueprint": self.name})
|
||||
app._apply_signal(future)
|
||||
|
||||
self.routes = [route for route in routes if isinstance(route, Route)]
|
||||
|
||||
# Deprecate these in 21.6
|
||||
self.websocket_routes = [
|
||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||
self.websocket_routes += [
|
||||
route for route in self.routes if route.ctx.websocket
|
||||
]
|
||||
self.middlewares = middleware
|
||||
self.exceptions = exception_handlers
|
||||
self.listeners = dict(listeners)
|
||||
self.middlewares += middleware
|
||||
self.exceptions += exception_handlers
|
||||
self.listeners.update(dict(listeners))
|
||||
|
||||
if self.registered:
|
||||
self.register_futures(
|
||||
self.apps,
|
||||
self,
|
||||
chain(
|
||||
registered,
|
||||
self._future_middleware,
|
||||
self._future_exceptions,
|
||||
self._future_listeners,
|
||||
self._future_signals,
|
||||
),
|
||||
)
|
||||
|
||||
async def dispatch(self, *args, **kwargs):
|
||||
condition = kwargs.pop("condition", {})
|
||||
@@ -288,3 +446,46 @@ class Blueprint(BaseSanic):
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_value(*values):
|
||||
value = values[-1]
|
||||
for v in values:
|
||||
if v is not None:
|
||||
value = v
|
||||
break
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def register_futures(
|
||||
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
|
||||
):
|
||||
for app in apps:
|
||||
app._future_registry.update(set((bp, item) for item in futures))
|
||||
|
||||
def pre_register(
|
||||
self,
|
||||
name: Union[str, Default],
|
||||
url_prefix: Optional[str] = None,
|
||||
host: Optional[Union[List[str], str]] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version_prefix: Union[str, Default] = _default,
|
||||
) -> None:
|
||||
if not hasattr(self.ctx, "_prereg"):
|
||||
self.ctx._prereg = []
|
||||
self.ctx._prereg.append(name)
|
||||
self.__class__.__pre_registry__[name].append(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
"bp": self,
|
||||
"url_prefix": url_prefix,
|
||||
"host": host,
|
||||
"version": version,
|
||||
"strict_slashes": strict_slashes,
|
||||
"version_prefix": version_prefix,
|
||||
}.items()
|
||||
if v is not None and v is not _default
|
||||
}
|
||||
)
|
||||
|
||||
0
sanic/cli/__init__.py
Normal file
0
sanic/cli/__init__.py
Normal file
202
sanic/cli/app.py
Normal file
202
sanic/cli/app.py
Normal file
@@ -0,0 +1,202 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from textwrap import indent
|
||||
from typing import Any, List, Union
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.application.logo import get_logo
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.cli.arguments import Group
|
||||
from sanic.helpers import _default
|
||||
from sanic.log import error_logger
|
||||
from sanic.simple import create_simple_server
|
||||
|
||||
|
||||
class SanicArgumentParser(ArgumentParser):
|
||||
...
|
||||
|
||||
|
||||
class SanicCLI:
|
||||
DEFAULT_APP_NAME = "SANIC"
|
||||
DESCRIPTION = indent(
|
||||
f"""
|
||||
{get_logo(True)}
|
||||
|
||||
To start running a Sanic application, provide a path to the module, where
|
||||
app is a Sanic() instance:
|
||||
|
||||
$ sanic path.to.server:app
|
||||
|
||||
Or, a path to a callable that returns a Sanic() instance:
|
||||
|
||||
$ sanic path.to.factory:create_app --factory
|
||||
|
||||
Or, a path to a directory to run as a simple HTTP server:
|
||||
|
||||
$ sanic ./path/to/static --simple
|
||||
""",
|
||||
prefix=" ",
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
width = shutil.get_terminal_size().columns
|
||||
self.parser = SanicArgumentParser(
|
||||
prog="sanic",
|
||||
description=self.DESCRIPTION,
|
||||
formatter_class=lambda prog: RawTextHelpFormatter(
|
||||
prog,
|
||||
max_help_position=36 if width > 96 else 24,
|
||||
indent_increment=4,
|
||||
width=None,
|
||||
),
|
||||
)
|
||||
self.parser._positionals.title = "Required\n========\n Positional"
|
||||
self.parser._optionals.title = "Optional\n========\n General"
|
||||
self.main_process = (
|
||||
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
|
||||
)
|
||||
self.args: List[Any] = []
|
||||
|
||||
def attach(self):
|
||||
for group in Group._registry:
|
||||
group.create(self.parser).attach()
|
||||
|
||||
def run(self):
|
||||
# This is to provide backwards compat -v to display version
|
||||
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
|
||||
parse_args = ["--version"] if legacy_version else None
|
||||
|
||||
self.args = self.parser.parse_args(args=parse_args)
|
||||
self._precheck()
|
||||
|
||||
try:
|
||||
app = self._get_app()
|
||||
kwargs = self._build_run_kwargs()
|
||||
app.run(**kwargs)
|
||||
except ValueError:
|
||||
error_logger.exception("Failed to run app")
|
||||
|
||||
def _precheck(self):
|
||||
if self.args.debug and self.main_process:
|
||||
error_logger.warning(
|
||||
"Starting in v22.3, --debug will no "
|
||||
"longer automatically run the auto-reloader.\n Switch to "
|
||||
"--dev to continue using that functionality."
|
||||
)
|
||||
|
||||
# # Custom TLS mismatch handling for better diagnostics
|
||||
if self.main_process and (
|
||||
# one of cert/key missing
|
||||
bool(self.args.cert) != bool(self.args.key)
|
||||
# new and old style self.args used together
|
||||
or self.args.tls
|
||||
and self.args.cert
|
||||
# strict host checking without certs would always fail
|
||||
or self.args.tlshost
|
||||
and not self.args.tls
|
||||
and not self.args.cert
|
||||
):
|
||||
self.parser.print_usage(sys.stderr)
|
||||
message = (
|
||||
"TLS certificates must be specified by either of:\n"
|
||||
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
|
||||
" --tls certdir (equivalent to the above)"
|
||||
)
|
||||
error_logger.error(message)
|
||||
sys.exit(1)
|
||||
|
||||
def _get_app(self):
|
||||
try:
|
||||
module_path = os.path.abspath(os.getcwd())
|
||||
if module_path not in sys.path:
|
||||
sys.path.append(module_path)
|
||||
|
||||
if self.args.simple:
|
||||
path = Path(self.args.module)
|
||||
app = create_simple_server(path)
|
||||
else:
|
||||
delimiter = ":" if ":" in self.args.module else "."
|
||||
module_name, app_name = self.args.module.rsplit(delimiter, 1)
|
||||
|
||||
if app_name.endswith("()"):
|
||||
self.args.factory = True
|
||||
app_name = app_name[:-2]
|
||||
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
if self.args.factory:
|
||||
app = app()
|
||||
|
||||
app_type_name = type(app).__name__
|
||||
|
||||
if isinstance(app, Blueprint):
|
||||
bp = app
|
||||
name = (
|
||||
bp.ctx._prereg[0]
|
||||
if hasattr(bp.ctx, "_prereg")
|
||||
else _default
|
||||
)
|
||||
if name is _default:
|
||||
name = self.DEFAULT_APP_NAME
|
||||
app = Sanic(name)
|
||||
elif not isinstance(app, Sanic):
|
||||
raise ValueError(
|
||||
f"Module is not a Sanic app, it is a {app_type_name}\n"
|
||||
f" Perhaps you meant {self.args.module}.app?"
|
||||
)
|
||||
except ImportError as e:
|
||||
if module_name.startswith(e.name):
|
||||
error_logger.error(
|
||||
f"No module named {e.name} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
return app
|
||||
|
||||
def _build_run_kwargs(self):
|
||||
ssl: Union[None, dict, str, list] = []
|
||||
if self.args.tlshost:
|
||||
ssl.append(None)
|
||||
if self.args.cert is not None or self.args.key is not None:
|
||||
ssl.append(dict(cert=self.args.cert, key=self.args.key))
|
||||
if self.args.tls:
|
||||
ssl += self.args.tls
|
||||
if not ssl:
|
||||
ssl = None
|
||||
elif len(ssl) == 1 and ssl[0] is not None:
|
||||
# Use only one cert, no TLSSelector.
|
||||
ssl = ssl[0]
|
||||
kwargs = {
|
||||
"access_log": self.args.access_log,
|
||||
"debug": self.args.debug,
|
||||
"fast": self.args.fast,
|
||||
"host": self.args.host,
|
||||
"motd": self.args.motd,
|
||||
"noisy_exceptions": self.args.noisy_exceptions,
|
||||
"port": self.args.port,
|
||||
"ssl": ssl,
|
||||
"unix": self.args.unix,
|
||||
"verbosity": self.args.verbosity or 0,
|
||||
"workers": self.args.workers,
|
||||
}
|
||||
|
||||
if self.args.auto_reload:
|
||||
kwargs["auto_reload"] = True
|
||||
|
||||
if self.args.path:
|
||||
if self.args.auto_reload or self.args.debug:
|
||||
kwargs["reload_dir"] = self.args.path
|
||||
else:
|
||||
error_logger.warning(
|
||||
"Ignoring '--reload-dir' since auto reloading was not "
|
||||
"enabled. If you would like to watch directories for "
|
||||
"changes, consider using --debug or --auto-reload."
|
||||
)
|
||||
return kwargs
|
||||
237
sanic/cli/arguments.py
Normal file
237
sanic/cli/arguments.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from argparse import ArgumentParser, _ArgumentGroup
|
||||
from typing import List, Optional, Type, Union
|
||||
|
||||
from sanic_routing import __version__ as __routing_version__ # type: ignore
|
||||
|
||||
from sanic import __version__
|
||||
|
||||
|
||||
class Group:
|
||||
name: Optional[str]
|
||||
container: Union[ArgumentParser, _ArgumentGroup]
|
||||
_registry: List[Type[Group]] = []
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
Group._registry.append(cls)
|
||||
|
||||
def __init__(self, parser: ArgumentParser, title: Optional[str]):
|
||||
self.parser = parser
|
||||
|
||||
if title:
|
||||
self.container = self.parser.add_argument_group(title=f" {title}")
|
||||
else:
|
||||
self.container = self.parser
|
||||
|
||||
@classmethod
|
||||
def create(cls, parser: ArgumentParser):
|
||||
instance = cls(parser, cls.name)
|
||||
return instance
|
||||
|
||||
def add_bool_arguments(self, *args, **kwargs):
|
||||
group = self.container.add_mutually_exclusive_group()
|
||||
kwargs["help"] = kwargs["help"].capitalize()
|
||||
group.add_argument(*args, action="store_true", **kwargs)
|
||||
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
|
||||
group.add_argument(
|
||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||
)
|
||||
|
||||
|
||||
class GeneralGroup(Group):
|
||||
name = None
|
||||
|
||||
def attach(self):
|
||||
self.container.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"Sanic {__version__}; Routing {__routing_version__}",
|
||||
)
|
||||
|
||||
self.container.add_argument(
|
||||
"module",
|
||||
help=(
|
||||
"Path to your Sanic app. Example: path.to.server:app\n"
|
||||
"If running a Simple Server, path to directory to serve. "
|
||||
"Example: ./\n"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ApplicationGroup(Group):
|
||||
name = "Application"
|
||||
|
||||
def attach(self):
|
||||
self.container.add_argument(
|
||||
"--factory",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Treat app as an application factory, "
|
||||
"i.e. a () -> <Sanic app> callable"
|
||||
),
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-s",
|
||||
"--simple",
|
||||
dest="simple",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Run Sanic as a Simple Server, and serve the contents of "
|
||||
"a directory\n(module arg should be a path)"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SocketGroup(Group):
|
||||
name = "Socket binding"
|
||||
|
||||
def attach(self):
|
||||
self.container.add_argument(
|
||||
"-H",
|
||||
"--host",
|
||||
dest="host",
|
||||
type=str,
|
||||
default="127.0.0.1",
|
||||
help="Host address [default 127.0.0.1]",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-p",
|
||||
"--port",
|
||||
dest="port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Port to serve on [default 8000]",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-u",
|
||||
"--unix",
|
||||
dest="unix",
|
||||
type=str,
|
||||
default="",
|
||||
help="location of unix socket",
|
||||
)
|
||||
|
||||
|
||||
class TLSGroup(Group):
|
||||
name = "TLS certificate"
|
||||
|
||||
def attach(self):
|
||||
self.container.add_argument(
|
||||
"--cert",
|
||||
dest="cert",
|
||||
type=str,
|
||||
help="Location of fullchain.pem, bundle.crt or equivalent",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"--key",
|
||||
dest="key",
|
||||
type=str,
|
||||
help="Location of privkey.pem or equivalent .key file",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"--tls",
|
||||
metavar="DIR",
|
||||
type=str,
|
||||
action="append",
|
||||
help=(
|
||||
"TLS certificate folder with fullchain.pem and privkey.pem\n"
|
||||
"May be specified multiple times to choose multiple "
|
||||
"certificates"
|
||||
),
|
||||
)
|
||||
self.container.add_argument(
|
||||
"--tls-strict-host",
|
||||
dest="tlshost",
|
||||
action="store_true",
|
||||
help="Only allow clients that send an SNI matching server certs",
|
||||
)
|
||||
|
||||
|
||||
class WorkerGroup(Group):
|
||||
name = "Worker"
|
||||
|
||||
def attach(self):
|
||||
group = self.container.add_mutually_exclusive_group()
|
||||
group.add_argument(
|
||||
"-w",
|
||||
"--workers",
|
||||
dest="workers",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of worker processes [default 1]",
|
||||
)
|
||||
group.add_argument(
|
||||
"--fast",
|
||||
dest="fast",
|
||||
action="store_true",
|
||||
help="Set the number of workers to max allowed",
|
||||
)
|
||||
self.add_bool_arguments(
|
||||
"--access-logs", dest="access_log", help="display access logs"
|
||||
)
|
||||
|
||||
|
||||
class DevelopmentGroup(Group):
|
||||
name = "Development"
|
||||
|
||||
def attach(self):
|
||||
self.container.add_argument(
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help="Run the server in debug mode",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-d",
|
||||
"--dev",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Currently is an alias for --debug. But starting in v22.3, \n"
|
||||
"--debug will no longer automatically trigger auto_restart. \n"
|
||||
"However, --dev will continue, effectively making it the \n"
|
||||
"same as debug + auto_reload."
|
||||
),
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-r",
|
||||
"--reload",
|
||||
"--auto-reload",
|
||||
dest="auto_reload",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Watch source directory for file changes and reload on "
|
||||
"changes"
|
||||
),
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-R",
|
||||
"--reload-dir",
|
||||
dest="path",
|
||||
action="append",
|
||||
help="Extra directories to watch and reload on changes",
|
||||
)
|
||||
|
||||
|
||||
class OutputGroup(Group):
|
||||
name = "Output"
|
||||
|
||||
def attach(self):
|
||||
self.add_bool_arguments(
|
||||
"--motd",
|
||||
dest="motd",
|
||||
default=True,
|
||||
help="Show the startup display",
|
||||
)
|
||||
self.container.add_argument(
|
||||
"-v",
|
||||
"--verbosity",
|
||||
action="count",
|
||||
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
|
||||
)
|
||||
self.add_bool_arguments(
|
||||
"--noisy-exceptions",
|
||||
dest="noisy_exceptions",
|
||||
help="Output stack traces for all exceptions",
|
||||
)
|
||||
@@ -10,6 +10,13 @@ from multidict import CIMultiDict # type: ignore
|
||||
OS_IS_WINDOWS = os.name == "nt"
|
||||
|
||||
|
||||
def enable_windows_color_support():
|
||||
import ctypes
|
||||
|
||||
kernel = ctypes.windll.kernel32
|
||||
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
|
||||
|
||||
|
||||
class Header(CIMultiDict):
|
||||
"""
|
||||
Container used for both request and response headers. It is a subclass of
|
||||
|
||||
200
sanic/config.py
200
sanic/config.py
@@ -1,58 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from inspect import isclass
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from .utils import load_module_from_file_location, str_to_bool
|
||||
from sanic.errorpages import check_error_format
|
||||
from sanic.http import Http
|
||||
from sanic.utils import load_module_from_file_location, str_to_bool
|
||||
|
||||
|
||||
if TYPE_CHECKING: # no cov
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
BASE_LOGO = """
|
||||
|
||||
Sanic
|
||||
Build Fast. Run Fast.
|
||||
|
||||
"""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
||||
"ACCESS_LOG": True,
|
||||
"AUTO_RELOAD": False,
|
||||
"EVENT_AUTOREGISTER": False,
|
||||
"FALLBACK_ERROR_FORMAT": "auto",
|
||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||
"FORWARDED_SECRET": None,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||
"KEEP_ALIVE": True,
|
||||
"MOTD": True,
|
||||
"MOTD_DISPLAY": {},
|
||||
"NOISY_EXCEPTIONS": False,
|
||||
"PROXIES_COUNT": None,
|
||||
"REAL_IP_HEADER": None,
|
||||
"REGISTER": True,
|
||||
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
|
||||
"REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384
|
||||
"REQUEST_ID_HEADER": "X-Request-ID",
|
||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||
"KEEP_ALIVE": True,
|
||||
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||
"WEBSOCKET_MAX_QUEUE": 32,
|
||||
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||
"WEBSOCKET_PING_INTERVAL": 20,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"ACCESS_LOG": True,
|
||||
"FORWARDED_SECRET": None,
|
||||
"REAL_IP_HEADER": None,
|
||||
"PROXIES_COUNT": None,
|
||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||
"REQUEST_ID_HEADER": "X-Request-ID",
|
||||
"FALLBACK_ERROR_FORMAT": "html",
|
||||
"REGISTER": True,
|
||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||
}
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||
ACCESS_LOG: bool
|
||||
AUTO_RELOAD: bool
|
||||
EVENT_AUTOREGISTER: bool
|
||||
FALLBACK_ERROR_FORMAT: str
|
||||
FORWARDED_FOR_HEADER: str
|
||||
FORWARDED_SECRET: Optional[str]
|
||||
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
||||
KEEP_ALIVE_TIMEOUT: int
|
||||
KEEP_ALIVE: bool
|
||||
NOISY_EXCEPTIONS: bool
|
||||
MOTD: bool
|
||||
MOTD_DISPLAY: Dict[str, str]
|
||||
PROXIES_COUNT: Optional[int]
|
||||
REAL_IP_HEADER: Optional[str]
|
||||
REGISTER: bool
|
||||
REQUEST_BUFFER_SIZE: int
|
||||
REQUEST_MAX_HEADER_SIZE: int
|
||||
REQUEST_ID_HEADER: str
|
||||
REQUEST_MAX_SIZE: int
|
||||
REQUEST_TIMEOUT: int
|
||||
RESPONSE_TIMEOUT: int
|
||||
SERVER_NAME: str
|
||||
WEBSOCKET_MAX_SIZE: int
|
||||
WEBSOCKET_PING_INTERVAL: int
|
||||
WEBSOCKET_PING_TIMEOUT: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
|
||||
load_env: Optional[Union[bool, str]] = True,
|
||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||
keep_alive: Optional[bool] = None,
|
||||
*,
|
||||
app: Optional[Sanic] = None,
|
||||
):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
|
||||
self.LOGO = BASE_LOGO
|
||||
self._app = app
|
||||
self._LOGO = ""
|
||||
|
||||
if keep_alive is not None:
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
|
||||
if load_env:
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
self.load_environment_vars(prefix=prefix)
|
||||
if env_prefix != SANIC_PREFIX:
|
||||
if env_prefix:
|
||||
self.load_environment_vars(env_prefix)
|
||||
elif load_env is not True:
|
||||
if load_env:
|
||||
self.load_environment_vars(prefix=load_env)
|
||||
warn(
|
||||
"Use of load_env is deprecated and will be removed in "
|
||||
"21.12. Modify the configuration prefix by passing "
|
||||
"env_prefix instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
self.load_environment_vars(SANIC_PREFIX)
|
||||
|
||||
self._configure_header_size()
|
||||
self._check_error_format()
|
||||
self._init = True
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -60,16 +116,69 @@ class Config(dict):
|
||||
except KeyError as ke:
|
||||
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
self[attr] = value
|
||||
def __setattr__(self, attr, value) -> None:
|
||||
self.update({attr: value})
|
||||
|
||||
def __setitem__(self, attr, value) -> None:
|
||||
self.update({attr: value})
|
||||
|
||||
def update(self, *other, **kwargs) -> None:
|
||||
other_mapping = {k: v for item in other for k, v in dict(item).items()}
|
||||
super().update(*other, **kwargs)
|
||||
for attr, value in {**other_mapping, **kwargs}.items():
|
||||
self._post_set(attr, value)
|
||||
|
||||
def _post_set(self, attr, value) -> None:
|
||||
if self.get("_init"):
|
||||
if attr in (
|
||||
"REQUEST_MAX_HEADER_SIZE",
|
||||
"REQUEST_BUFFER_SIZE",
|
||||
"REQUEST_MAX_SIZE",
|
||||
):
|
||||
self._configure_header_size()
|
||||
elif attr == "FALLBACK_ERROR_FORMAT":
|
||||
self._check_error_format()
|
||||
if self.app and value != self.app.error_handler.fallback:
|
||||
if self.app.error_handler.fallback != "auto":
|
||||
warn(
|
||||
"Overriding non-default ErrorHandler fallback "
|
||||
"value. Changing from "
|
||||
f"{self.app.error_handler.fallback} to {value}."
|
||||
)
|
||||
self.app.error_handler.fallback = value
|
||||
elif attr == "LOGO":
|
||||
self._LOGO = value
|
||||
warn(
|
||||
"Setting the config.LOGO is deprecated and will no longer "
|
||||
"be supported starting in v22.6.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
return self._app
|
||||
|
||||
@property
|
||||
def LOGO(self):
|
||||
return self._LOGO
|
||||
|
||||
def _configure_header_size(self):
|
||||
Http.set_header_max_size(
|
||||
self.REQUEST_MAX_HEADER_SIZE,
|
||||
self.REQUEST_BUFFER_SIZE - 4096,
|
||||
self.REQUEST_MAX_SIZE,
|
||||
)
|
||||
|
||||
def _check_error_format(self):
|
||||
check_error_format(self.FALLBACK_ERROR_FORMAT)
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present. This is called automatically when
|
||||
Sanic starts up to load environment variables into config.
|
||||
Looks for prefixed environment variables and applies them to the
|
||||
configuration if present. This is called automatically when Sanic
|
||||
starts up to load environment variables into config.
|
||||
|
||||
It will automatically hyrdate the following types:
|
||||
It will automatically hydrate the following types:
|
||||
|
||||
- ``int``
|
||||
- ``float``
|
||||
@@ -77,19 +186,18 @@ class Config(dict):
|
||||
|
||||
Anything else will be imported as a ``str``.
|
||||
"""
|
||||
for k, v in environ.items():
|
||||
if k.startswith(prefix):
|
||||
_, config_key = k.split(prefix, 1)
|
||||
for key, value in environ.items():
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
|
||||
_, config_key = key.split(prefix, 1)
|
||||
|
||||
for converter in (int, float, str_to_bool, str):
|
||||
try:
|
||||
self[config_key] = int(v)
|
||||
self[config_key] = converter(value)
|
||||
break
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = str_to_bool(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
pass
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""
|
||||
|
||||
@@ -1,2 +1,28 @@
|
||||
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class HTTPMethod(str, Enum):
|
||||
def _generate_next_value_(name, start, count, last_values):
|
||||
return name.upper()
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
value = str(value).upper()
|
||||
return super().__eq__(value)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
GET = auto()
|
||||
POST = auto()
|
||||
PUT = auto()
|
||||
HEAD = auto()
|
||||
OPTIONS = auto()
|
||||
PATCH = auto()
|
||||
DELETE = auto()
|
||||
|
||||
|
||||
HTTP_METHODS = tuple(HTTPMethod.__members__.values())
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
@@ -25,12 +25,13 @@ from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, html, json, text
|
||||
|
||||
|
||||
dumps: t.Callable[..., str]
|
||||
try:
|
||||
from ujson import dumps
|
||||
|
||||
dumps = partial(dumps, escape_forward_slashes=False)
|
||||
except ImportError: # noqa
|
||||
from json import dumps # type: ignore
|
||||
from json import dumps
|
||||
|
||||
|
||||
FALLBACK_TEXT = (
|
||||
@@ -45,6 +46,8 @@ class BaseRenderer:
|
||||
Base class that all renderers must inherit from.
|
||||
"""
|
||||
|
||||
dumps = staticmethod(dumps)
|
||||
|
||||
def __init__(self, request, exception, debug):
|
||||
self.request = request
|
||||
self.exception = exception
|
||||
@@ -112,14 +115,16 @@ class HTMLRenderer(BaseRenderer):
|
||||
TRACEBACK_STYLE = """
|
||||
html { font-family: sans-serif }
|
||||
h2 { color: #888; }
|
||||
.tb-wrapper p { margin: 0 }
|
||||
.tb-wrapper p, dl, dd { margin: 0 }
|
||||
.frame-border { margin: 1rem }
|
||||
.frame-line > * { padding: 0.3rem 0.6rem }
|
||||
.frame-line { margin-bottom: 0.3rem }
|
||||
.frame-code { font-size: 16px; padding-left: 4ch }
|
||||
.tb-wrapper { border: 1px solid #eee }
|
||||
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
|
||||
.frame-descriptor { background: #e2eafb; font-size: 14px }
|
||||
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
|
||||
.frame-line, dl { margin-bottom: 0.3rem }
|
||||
.frame-code, dd { font-size: 16px; padding-left: 4ch }
|
||||
.tb-wrapper, dl { border: 1px solid #eee }
|
||||
.tb-header,.obj-header {
|
||||
background: #eee; padding: 0.3rem; font-weight: bold
|
||||
}
|
||||
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
|
||||
"""
|
||||
TRACEBACK_WRAPPER_HTML = (
|
||||
"<div class=tb-header>{exc_name}: {exc_value}</div>"
|
||||
@@ -138,6 +143,11 @@ class HTMLRenderer(BaseRenderer):
|
||||
"<p class=frame-code><code>{0.line}</code>"
|
||||
"</div>"
|
||||
)
|
||||
OBJECT_WRAPPER_HTML = (
|
||||
"<div class=obj-header>{title}</div>"
|
||||
"<dl class={obj_type}>{display_html}</dl>"
|
||||
)
|
||||
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
|
||||
OUTPUT_HTML = (
|
||||
"<!DOCTYPE html><html lang=en>"
|
||||
"<meta charset=UTF-8><title>{title}</title>\n"
|
||||
@@ -152,7 +162,7 @@ class HTMLRenderer(BaseRenderer):
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
style=self.TRACEBACK_STYLE,
|
||||
body=self._generate_body(),
|
||||
body=self._generate_body(full=True),
|
||||
),
|
||||
status=self.status,
|
||||
)
|
||||
@@ -163,7 +173,7 @@ class HTMLRenderer(BaseRenderer):
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
style=self.TRACEBACK_STYLE,
|
||||
body="",
|
||||
body=self._generate_body(full=False),
|
||||
),
|
||||
status=self.status,
|
||||
headers=self.headers,
|
||||
@@ -177,27 +187,49 @@ class HTMLRenderer(BaseRenderer):
|
||||
def title(self):
|
||||
return escape(f"⚠️ {super().title}")
|
||||
|
||||
def _generate_body(self):
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
def _generate_body(self, *, full):
|
||||
lines = []
|
||||
if full:
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
||||
appname = escape(self.request.app.name)
|
||||
name = escape(self.exception.__class__.__name__)
|
||||
value = escape(self.exception)
|
||||
path = escape(self.request.path)
|
||||
lines += [
|
||||
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
|
||||
f"{traceback_html}",
|
||||
"<div class=summary><p>",
|
||||
f"<b>{name}: {value}</b> "
|
||||
f"while handling path <code>{path}</code>",
|
||||
"</div>",
|
||||
]
|
||||
|
||||
for attr, display in (("context", True), ("extra", bool(full))):
|
||||
info = getattr(self.exception, attr, None)
|
||||
if info and display:
|
||||
lines.append(self._generate_object_display(info, attr))
|
||||
|
||||
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
||||
appname = escape(self.request.app.name)
|
||||
name = escape(self.exception.__class__.__name__)
|
||||
value = escape(self.exception)
|
||||
path = escape(self.request.path)
|
||||
lines = [
|
||||
f"<h2>Traceback of {appname} (most recent call last):</h2>",
|
||||
f"{traceback_html}",
|
||||
"<div class=summary><p>",
|
||||
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
|
||||
"</div>",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def _generate_object_display(
|
||||
self, obj: t.Dict[str, t.Any], descriptor: str
|
||||
) -> str:
|
||||
display = "".join(
|
||||
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
|
||||
for key, value in obj.items()
|
||||
)
|
||||
return self.OBJECT_WRAPPER_HTML.format(
|
||||
title=descriptor.title(),
|
||||
display_html=display,
|
||||
obj_type=descriptor.lower(),
|
||||
)
|
||||
|
||||
def _format_exc(self, exc):
|
||||
frames = extract_tb(exc.__traceback__)
|
||||
frame_html = "".join(
|
||||
@@ -224,7 +256,7 @@ class TextRenderer(BaseRenderer):
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
bar=("=" * len(self.title)),
|
||||
body=self._generate_body(),
|
||||
body=self._generate_body(full=True),
|
||||
),
|
||||
status=self.status,
|
||||
)
|
||||
@@ -235,7 +267,7 @@ class TextRenderer(BaseRenderer):
|
||||
title=self.title,
|
||||
text=self.text,
|
||||
bar=("=" * len(self.title)),
|
||||
body="",
|
||||
body=self._generate_body(full=False),
|
||||
),
|
||||
status=self.status,
|
||||
headers=self.headers,
|
||||
@@ -245,21 +277,31 @@ class TextRenderer(BaseRenderer):
|
||||
def title(self):
|
||||
return f"⚠️ {super().title}"
|
||||
|
||||
def _generate_body(self):
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
def _generate_body(self, *, full):
|
||||
lines = []
|
||||
if full:
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
lines = [
|
||||
f"{self.exception.__class__.__name__}: {self.exception} while "
|
||||
f"handling path {self.request.path}",
|
||||
f"Traceback of {self.request.app.name} (most recent call last):\n",
|
||||
]
|
||||
lines += [
|
||||
f"{self.exception.__class__.__name__}: {self.exception} while "
|
||||
f"handling path {self.request.path}",
|
||||
f"Traceback of {self.request.app.name} "
|
||||
"(most recent call last):\n",
|
||||
]
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
while exc_value:
|
||||
exceptions.append(self._format_exc(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
return "\n".join(lines + exceptions[::-1])
|
||||
lines += exceptions[::-1]
|
||||
|
||||
for attr, display in (("context", True), ("extra", bool(full))):
|
||||
info = getattr(self.exception, attr, None)
|
||||
if info and display:
|
||||
lines += self._generate_object_display_list(info, attr)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_exc(self, exc):
|
||||
frames = "\n\n".join(
|
||||
@@ -272,6 +314,13 @@ class TextRenderer(BaseRenderer):
|
||||
)
|
||||
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
|
||||
|
||||
def _generate_object_display_list(self, obj, descriptor):
|
||||
lines = [f"\n{descriptor.title()}"]
|
||||
for key, value in obj.items():
|
||||
display = self.dumps(value)
|
||||
lines.append(f"{self.SPACER * 2}{key}: {display}")
|
||||
return lines
|
||||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
"""
|
||||
@@ -280,11 +329,11 @@ class JSONRenderer(BaseRenderer):
|
||||
|
||||
def full(self) -> HTTPResponse:
|
||||
output = self._generate_output(full=True)
|
||||
return json(output, status=self.status, dumps=dumps)
|
||||
return json(output, status=self.status, dumps=self.dumps)
|
||||
|
||||
def minimal(self) -> HTTPResponse:
|
||||
output = self._generate_output(full=False)
|
||||
return json(output, status=self.status, dumps=dumps)
|
||||
return json(output, status=self.status, dumps=self.dumps)
|
||||
|
||||
def _generate_output(self, *, full):
|
||||
output = {
|
||||
@@ -293,6 +342,11 @@ class JSONRenderer(BaseRenderer):
|
||||
"message": self.text,
|
||||
}
|
||||
|
||||
for attr, display in (("context", True), ("extra", bool(full))):
|
||||
info = getattr(self.exception, attr, None)
|
||||
if info and display:
|
||||
output[attr] = info
|
||||
|
||||
if full:
|
||||
_, exc_value, __ = sys.exc_info()
|
||||
exceptions = []
|
||||
@@ -340,41 +394,139 @@ RENDERERS_BY_CONFIG = {
|
||||
}
|
||||
|
||||
RENDERERS_BY_CONTENT_TYPE = {
|
||||
"multipart/form-data": HTMLRenderer,
|
||||
"application/json": JSONRenderer,
|
||||
"text/plain": TextRenderer,
|
||||
"application/json": JSONRenderer,
|
||||
"multipart/form-data": HTMLRenderer,
|
||||
"text/html": HTMLRenderer,
|
||||
}
|
||||
CONTENT_TYPE_BY_RENDERERS = {
|
||||
v: k for k, v in RENDERERS_BY_CONTENT_TYPE.items()
|
||||
}
|
||||
|
||||
RESPONSE_MAPPING = {
|
||||
"empty": "html",
|
||||
"json": "json",
|
||||
"text": "text",
|
||||
"raw": "text",
|
||||
"html": "html",
|
||||
"file": "html",
|
||||
"file_stream": "text",
|
||||
"stream": "text",
|
||||
"redirect": "html",
|
||||
"text/plain": "text",
|
||||
"text/html": "html",
|
||||
"application/json": "json",
|
||||
}
|
||||
|
||||
|
||||
def check_error_format(format):
|
||||
if format not in RENDERERS_BY_CONFIG and format != "auto":
|
||||
raise SanicException(f"Unknown format: {format}")
|
||||
|
||||
|
||||
def exception_response(
|
||||
request: Request,
|
||||
exception: Exception,
|
||||
debug: bool,
|
||||
fallback: str,
|
||||
base: t.Type[BaseRenderer],
|
||||
renderer: t.Type[t.Optional[BaseRenderer]] = None,
|
||||
) -> HTTPResponse:
|
||||
"""
|
||||
Render a response for the default FALLBACK exception handler.
|
||||
"""
|
||||
content_type = None
|
||||
|
||||
if not renderer:
|
||||
renderer = HTMLRenderer
|
||||
# Make sure we have something set
|
||||
renderer = base
|
||||
render_format = fallback
|
||||
|
||||
if request:
|
||||
if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
|
||||
# If there is a request, try and get the format
|
||||
# from the route
|
||||
if request.route:
|
||||
try:
|
||||
renderer = JSONRenderer if request.json else HTMLRenderer
|
||||
except InvalidUsage:
|
||||
if request.route.ctx.error_format:
|
||||
render_format = request.route.ctx.error_format
|
||||
except AttributeError:
|
||||
...
|
||||
|
||||
content_type = request.headers.getone("content-type", "").split(
|
||||
";"
|
||||
)[0]
|
||||
|
||||
acceptable = request.accept
|
||||
|
||||
# If the format is auto still, make a guess
|
||||
if render_format == "auto":
|
||||
# First, if there is an Accept header, check if text/html
|
||||
# is the first option
|
||||
# According to MDN Web Docs, all major browsers use text/html
|
||||
# as the primary value in Accept (with the exception of IE 8,
|
||||
# and, well, if you are supporting IE 8, then you have bigger
|
||||
# problems to concern yourself with than what default exception
|
||||
# renderer is used)
|
||||
# Source:
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values
|
||||
|
||||
if acceptable and acceptable[0].match(
|
||||
"text/html",
|
||||
allow_type_wildcard=False,
|
||||
allow_subtype_wildcard=False,
|
||||
):
|
||||
renderer = HTMLRenderer
|
||||
|
||||
content_type, *_ = request.headers.get(
|
||||
"content-type", ""
|
||||
).split(";")
|
||||
renderer = RENDERERS_BY_CONTENT_TYPE.get(
|
||||
content_type, renderer
|
||||
)
|
||||
# Second, if there is an Accept header, check if
|
||||
# application/json is an option, or if the content-type
|
||||
# is application/json
|
||||
elif (
|
||||
acceptable
|
||||
and acceptable.match(
|
||||
"application/json",
|
||||
allow_type_wildcard=False,
|
||||
allow_subtype_wildcard=False,
|
||||
)
|
||||
or content_type == "application/json"
|
||||
):
|
||||
renderer = JSONRenderer
|
||||
|
||||
# Third, if there is no Accept header, assume we want text.
|
||||
# The likely use case here is a raw socket.
|
||||
elif not acceptable:
|
||||
renderer = TextRenderer
|
||||
else:
|
||||
# Fourth, look to see if there was a JSON body
|
||||
# When in this situation, the request is probably coming
|
||||
# from curl, an API client like Postman or Insomnia, or a
|
||||
# package like requests or httpx
|
||||
try:
|
||||
# Give them the benefit of the doubt if they did:
|
||||
# $ curl localhost:8000 -d '{"foo": "bar"}'
|
||||
# And provide them with JSONRenderer
|
||||
renderer = JSONRenderer if request.json else base
|
||||
except InvalidUsage:
|
||||
renderer = base
|
||||
else:
|
||||
render_format = request.app.config.FALLBACK_ERROR_FORMAT
|
||||
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
|
||||
|
||||
# Lastly, if there is an Accept header, make sure
|
||||
# our choice is okay
|
||||
if acceptable:
|
||||
type_ = CONTENT_TYPE_BY_RENDERERS.get(renderer) # type: ignore
|
||||
if type_ and type_ not in acceptable:
|
||||
# If the renderer selected is not in the Accept header
|
||||
# look through what is in the Accept header, and select
|
||||
# the first option that matches. Otherwise, just drop back
|
||||
# to the original default
|
||||
for accept in acceptable:
|
||||
mtype = f"{accept.type_}/{accept.subtype}"
|
||||
maybe = RENDERERS_BY_CONTENT_TYPE.get(mtype)
|
||||
if maybe:
|
||||
renderer = maybe
|
||||
break
|
||||
else:
|
||||
renderer = base
|
||||
|
||||
renderer = t.cast(t.Type[BaseRenderer], renderer)
|
||||
return renderer(request, exception, debug).render()
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
|
||||
_sanic_exceptions = {}
|
||||
|
||||
|
||||
def add_status_code(code, quiet=None):
|
||||
"""
|
||||
Decorator used for adding exceptions to :class:`SanicException`.
|
||||
"""
|
||||
|
||||
def class_decorator(cls):
|
||||
cls.status_code = code
|
||||
if quiet or quiet is None and code != 500:
|
||||
cls.quiet = True
|
||||
_sanic_exceptions[code] = cls
|
||||
return cls
|
||||
|
||||
return class_decorator
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
def __init__(self, message, status_code=None, quiet=None):
|
||||
message: str = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: Optional[Union[str, bytes]] = None,
|
||||
status_code: Optional[int] = None,
|
||||
quiet: Optional[bool] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
self.context = context
|
||||
self.extra = extra
|
||||
if message is None:
|
||||
if self.message:
|
||||
message = self.message
|
||||
elif status_code is not None:
|
||||
msg: bytes = STATUS_CODES.get(status_code, b"")
|
||||
message = msg.decode("utf8")
|
||||
|
||||
super().__init__(message)
|
||||
|
||||
if status_code is not None:
|
||||
@@ -33,45 +33,45 @@ class SanicException(Exception):
|
||||
self.quiet = True
|
||||
|
||||
|
||||
@add_status_code(404)
|
||||
class NotFound(SanicException):
|
||||
"""
|
||||
**Status**: 404 Not Found
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 404
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(400)
|
||||
class InvalidUsage(SanicException):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 400
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(405)
|
||||
class MethodNotSupported(SanicException):
|
||||
"""
|
||||
**Status**: 405 Method Not Allowed
|
||||
"""
|
||||
|
||||
status_code = 405
|
||||
quiet = True
|
||||
|
||||
def __init__(self, message, method, allowed_methods):
|
||||
super().__init__(message)
|
||||
self.headers = {"Allow": ", ".join(allowed_methods)}
|
||||
|
||||
|
||||
@add_status_code(500)
|
||||
class ServerError(SanicException):
|
||||
"""
|
||||
**Status**: 500 Internal Server Error
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 500
|
||||
|
||||
|
||||
@add_status_code(503)
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""
|
||||
**Status**: 503 Service Unavailable
|
||||
@@ -80,7 +80,8 @@ class ServiceUnavailable(SanicException):
|
||||
down for maintenance). Generally, this is a temporary state.
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 503
|
||||
quiet = True
|
||||
|
||||
|
||||
class URLBuildError(ServerError):
|
||||
@@ -88,7 +89,7 @@ class URLBuildError(ServerError):
|
||||
**Status**: 500 Internal Server Error
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 500
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
@@ -102,7 +103,6 @@ class FileNotFound(NotFound):
|
||||
self.relative_url = relative_url
|
||||
|
||||
|
||||
@add_status_code(408)
|
||||
class RequestTimeout(SanicException):
|
||||
"""The Web server (running the Web site) thinks that there has been too
|
||||
long an interval of time between 1) the establishment of an IP
|
||||
@@ -112,16 +112,17 @@ class RequestTimeout(SanicException):
|
||||
server has 'timed out' on that particular socket connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 408
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(413)
|
||||
class PayloadTooLarge(SanicException):
|
||||
"""
|
||||
**Status**: 413 Payload Too Large
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 413
|
||||
quiet = True
|
||||
|
||||
|
||||
class HeaderNotFound(InvalidUsage):
|
||||
@@ -129,36 +130,42 @@ class HeaderNotFound(InvalidUsage):
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class InvalidHeader(InvalidUsage):
|
||||
"""
|
||||
**Status**: 400 Bad Request
|
||||
"""
|
||||
|
||||
|
||||
@add_status_code(416)
|
||||
class ContentRangeError(SanicException):
|
||||
"""
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
status_code = 416
|
||||
quiet = True
|
||||
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
||||
|
||||
|
||||
@add_status_code(417)
|
||||
class HeaderExpectationFailed(SanicException):
|
||||
"""
|
||||
**Status**: 417 Expectation Failed
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 417
|
||||
quiet = True
|
||||
|
||||
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
"""
|
||||
**Status**: 403 Forbidden
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 403
|
||||
quiet = True
|
||||
|
||||
|
||||
class InvalidRangeType(ContentRangeError):
|
||||
@@ -166,7 +173,8 @@ class InvalidRangeType(ContentRangeError):
|
||||
**Status**: 416 Range Not Satisfiable
|
||||
"""
|
||||
|
||||
pass
|
||||
status_code = 416
|
||||
quiet = True
|
||||
|
||||
|
||||
class PyFileError(Exception):
|
||||
@@ -174,7 +182,6 @@ class PyFileError(Exception):
|
||||
super().__init__("could not execute config file %s", file)
|
||||
|
||||
|
||||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
**Status**: 401 Unauthorized
|
||||
@@ -210,6 +217,9 @@ class Unauthorized(SanicException):
|
||||
realm="Restricted Area")
|
||||
"""
|
||||
|
||||
status_code = 401
|
||||
quiet = True
|
||||
|
||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||
super().__init__(message, status_code)
|
||||
|
||||
@@ -231,6 +241,11 @@ class InvalidSignal(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
class WebsocketClosed(SanicException):
|
||||
quiet = True
|
||||
message = "Client has closed the websocket connection"
|
||||
|
||||
|
||||
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
@@ -241,9 +256,13 @@ def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||
:param status_code: The HTTP status code to return.
|
||||
:param message: The HTTP response body. Defaults to the messages in
|
||||
"""
|
||||
if message is None:
|
||||
msg: bytes = STATUS_CODES[status_code]
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = msg.decode("utf8")
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
raise sanic_exception(message=message, status_code=status_code)
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"sanic.exceptions.abort has been marked as deprecated, and will be "
|
||||
"removed in release 21.12.\n To migrate your code, simply replace "
|
||||
"abort(status_code, msg) with raise SanicException(msg, status_code), "
|
||||
"or even better, raise an appropriate SanicException subclass."
|
||||
)
|
||||
|
||||
raise SanicException(message=message, status_code=status_code)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from traceback import format_exc
|
||||
from inspect import signature
|
||||
from typing import Dict, List, Optional, Tuple, Type
|
||||
from warnings import warn
|
||||
|
||||
from sanic.errorpages import exception_response
|
||||
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
)
|
||||
from sanic.log import logger
|
||||
from sanic.log import error_logger
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
@@ -23,16 +26,53 @@ class ErrorHandler:
|
||||
|
||||
"""
|
||||
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = []
|
||||
self.cached_handlers = {}
|
||||
# Beginning in v22.3, the base renderer will be TextRenderer
|
||||
def __init__(
|
||||
self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer
|
||||
):
|
||||
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
|
||||
self.cached_handlers: Dict[
|
||||
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
||||
] = {}
|
||||
self.debug = False
|
||||
self.fallback = fallback
|
||||
self.base = base
|
||||
|
||||
def add(self, exception, handler):
|
||||
@classmethod
|
||||
def finalize(cls, error_handler, fallback: Optional[str] = None):
|
||||
if (
|
||||
fallback
|
||||
and fallback != "auto"
|
||||
and error_handler.fallback == "auto"
|
||||
):
|
||||
error_handler.fallback = fallback
|
||||
|
||||
if not isinstance(error_handler, cls):
|
||||
error_logger.warning(
|
||||
f"Error handler is non-conforming: {type(error_handler)}"
|
||||
)
|
||||
|
||||
sig = signature(error_handler.lookup)
|
||||
if len(sig.parameters) == 1:
|
||||
warn(
|
||||
"You are using a deprecated error handler. The lookup "
|
||||
"method should accept two positional parameters: "
|
||||
"(exception, route_name: Optional[str]). "
|
||||
"Until you upgrade your ErrorHandler.lookup, Blueprint "
|
||||
"specific exceptions will not work properly. Beginning "
|
||||
"in v22.3, the legacy style lookup method will not "
|
||||
"work at all.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
error_handler._lookup = error_handler._legacy_lookup
|
||||
|
||||
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
||||
return self.lookup(exception, route_name)
|
||||
|
||||
def _legacy_lookup(self, exception, route_name: Optional[str] = None):
|
||||
return self.lookup(exception)
|
||||
|
||||
def add(self, exception, handler, route_names: Optional[List[str]] = None):
|
||||
"""
|
||||
Add a new exception handler to an already existing handler object.
|
||||
|
||||
@@ -45,9 +85,16 @@ class ErrorHandler:
|
||||
|
||||
:return: None
|
||||
"""
|
||||
# self.handlers is deprecated and will be removed in version 22.3
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
if route_names:
|
||||
for route in route_names:
|
||||
self.cached_handlers[(exception, route)] = handler
|
||||
else:
|
||||
self.cached_handlers[(exception, None)] = handler
|
||||
|
||||
def lookup(self, exception, route_name: Optional[str] = None):
|
||||
"""
|
||||
Lookup the existing instance of :class:`ErrorHandler` and fetch the
|
||||
registered handler for a specific type of exception.
|
||||
@@ -61,16 +108,32 @@ class ErrorHandler:
|
||||
|
||||
:return: Registered function if found ``None`` otherwise
|
||||
"""
|
||||
handler = self.cached_handlers.get(type(exception), self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
self.cached_handlers[type(exception)] = handler
|
||||
exception_class = type(exception)
|
||||
|
||||
for name in (route_name, None):
|
||||
exception_key = (exception_class, name)
|
||||
handler = self.cached_handlers.get(exception_key)
|
||||
if handler:
|
||||
return handler
|
||||
|
||||
for name in (route_name, None):
|
||||
for ancestor in type.mro(exception_class):
|
||||
exception_key = (ancestor, name)
|
||||
if exception_key in self.cached_handlers:
|
||||
handler = self.cached_handlers[exception_key]
|
||||
self.cached_handlers[
|
||||
(exception_class, route_name)
|
||||
] = handler
|
||||
return handler
|
||||
self.cached_handlers[type(exception)] = None
|
||||
handler = None
|
||||
|
||||
if ancestor is BaseException:
|
||||
break
|
||||
self.cached_handlers[(exception_class, route_name)] = None
|
||||
handler = None
|
||||
return handler
|
||||
|
||||
_lookup = _full_lookup
|
||||
|
||||
def response(self, request, exception):
|
||||
"""Fetches and executes an exception handler and returns a response
|
||||
object
|
||||
@@ -85,7 +148,8 @@ class ErrorHandler:
|
||||
:return: Wrap the return value obtained from :func:`default`
|
||||
or registered handler for that type of exception.
|
||||
"""
|
||||
handler = self.lookup(exception)
|
||||
route_name = request.name if request else None
|
||||
handler = self._lookup(exception, route_name)
|
||||
response = None
|
||||
try:
|
||||
if handler:
|
||||
@@ -93,7 +157,6 @@ class ErrorHandler:
|
||||
if response is None:
|
||||
response = self.default(request, exception)
|
||||
except Exception:
|
||||
self.log(format_exc())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
@@ -101,7 +164,7 @@ class ErrorHandler:
|
||||
response_message = (
|
||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||
)
|
||||
logger.exception(response_message, handler.__name__, url)
|
||||
error_logger.exception(response_message, handler.__name__, url)
|
||||
|
||||
if self.debug:
|
||||
return text(response_message % (handler.__name__, url), 500)
|
||||
@@ -109,11 +172,6 @@ class ErrorHandler:
|
||||
return text("An error occurred while handling an error", 500)
|
||||
return response
|
||||
|
||||
def log(self, message, level="error"):
|
||||
"""
|
||||
Deprecated, do not use.
|
||||
"""
|
||||
|
||||
def default(self, request, exception):
|
||||
"""
|
||||
Provide a default behavior for the objects of :class:`ErrorHandler`.
|
||||
@@ -129,17 +187,28 @@ class ErrorHandler:
|
||||
:class:`Exception`
|
||||
:return:
|
||||
"""
|
||||
self.log(request, exception)
|
||||
return exception_response(
|
||||
request,
|
||||
exception,
|
||||
debug=self.debug,
|
||||
base=self.base,
|
||||
fallback=self.fallback,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log(request, exception):
|
||||
quiet = getattr(exception, "quiet", False)
|
||||
if quiet is False:
|
||||
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
|
||||
if quiet is False or noisy is True:
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
|
||||
self.log(format_exc())
|
||||
logger.exception("Exception occurred while handling uri: %s", url)
|
||||
|
||||
return exception_response(request, exception, self.debug)
|
||||
error_logger.exception(
|
||||
"Exception occurred while handling uri: %s", url
|
||||
)
|
||||
|
||||
|
||||
class ContentRangeHandler:
|
||||
@@ -165,7 +234,7 @@ class ContentRangeHandler:
|
||||
|
||||
def __init__(self, request, stats):
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.get("Range")
|
||||
_range = request.headers.getone("range", None)
|
||||
if _range is None:
|
||||
raise HeaderNotFound("Range Header Not Found")
|
||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||
|
||||
206
sanic/headers.py
206
sanic/headers.py
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic.exceptions import InvalidHeader
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
|
||||
@@ -25,11 +28,180 @@ _host_re = re.compile(
|
||||
|
||||
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
|
||||
# curl all have different escaping, that we try to handle as well as possible,
|
||||
# even though no client espaces in a way that would allow perfect handling.
|
||||
# even though no client escapes in a way that would allow perfect handling.
|
||||
|
||||
# For more information, consult ../tests/test_requests.py
|
||||
|
||||
|
||||
def parse_arg_as_accept(f):
|
||||
def func(self, other, *args, **kwargs):
|
||||
if not isinstance(other, Accept) and other:
|
||||
other = Accept.parse(other)
|
||||
return f(self, other, *args, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class MediaType(str):
|
||||
def __new__(cls, value: str):
|
||||
return str.__new__(cls, value)
|
||||
|
||||
def __init__(self, value: str) -> None:
|
||||
self.value = value
|
||||
self.is_wildcard = self.check_if_wildcard(value)
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.is_wildcard:
|
||||
return True
|
||||
|
||||
if self.match(other):
|
||||
return True
|
||||
|
||||
other_is_wildcard = (
|
||||
other.is_wildcard
|
||||
if isinstance(other, MediaType)
|
||||
else self.check_if_wildcard(other)
|
||||
)
|
||||
|
||||
return other_is_wildcard
|
||||
|
||||
def match(self, other):
|
||||
other_value = other.value if isinstance(other, MediaType) else other
|
||||
return self.value == other_value
|
||||
|
||||
@staticmethod
|
||||
def check_if_wildcard(value):
|
||||
return value == "*"
|
||||
|
||||
|
||||
class Accept(str):
|
||||
def __new__(cls, value: str, *args, **kwargs):
|
||||
return str.__new__(cls, value)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
value: str,
|
||||
type_: MediaType,
|
||||
subtype: MediaType,
|
||||
*,
|
||||
q: str = "1.0",
|
||||
**kwargs: str,
|
||||
):
|
||||
qvalue = float(q)
|
||||
if qvalue > 1 or qvalue < 0:
|
||||
raise InvalidHeader(
|
||||
f"Accept header qvalue must be between 0 and 1, not: {qvalue}"
|
||||
)
|
||||
self.value = value
|
||||
self.type_ = type_
|
||||
self.subtype = subtype
|
||||
self.qvalue = qvalue
|
||||
self.params = kwargs
|
||||
|
||||
def _compare(self, other, method):
|
||||
try:
|
||||
return method(self.qvalue, other.qvalue)
|
||||
except (AttributeError, TypeError):
|
||||
return NotImplemented
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __lt__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s < o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __le__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s <= o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __eq__(self, other: Union[str, Accept]): # type: ignore
|
||||
return self._compare(other, lambda s, o: s == o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __ge__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s >= o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __gt__(self, other: Union[str, Accept]):
|
||||
return self._compare(other, lambda s, o: s > o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def __ne__(self, other: Union[str, Accept]): # type: ignore
|
||||
return self._compare(other, lambda s, o: s != o)
|
||||
|
||||
@parse_arg_as_accept
|
||||
def match(
|
||||
self,
|
||||
other,
|
||||
*,
|
||||
allow_type_wildcard: bool = True,
|
||||
allow_subtype_wildcard: bool = True,
|
||||
) -> bool:
|
||||
type_match = (
|
||||
self.type_ == other.type_
|
||||
if allow_type_wildcard
|
||||
else (
|
||||
self.type_.match(other.type_)
|
||||
and not self.type_.is_wildcard
|
||||
and not other.type_.is_wildcard
|
||||
)
|
||||
)
|
||||
subtype_match = (
|
||||
self.subtype == other.subtype
|
||||
if allow_subtype_wildcard
|
||||
else (
|
||||
self.subtype.match(other.subtype)
|
||||
and not self.subtype.is_wildcard
|
||||
and not other.subtype.is_wildcard
|
||||
)
|
||||
)
|
||||
|
||||
return type_match and subtype_match
|
||||
|
||||
@classmethod
|
||||
def parse(cls, raw: str) -> Accept:
|
||||
invalid = False
|
||||
mtype = raw.strip()
|
||||
|
||||
try:
|
||||
media, *raw_params = mtype.split(";")
|
||||
type_, subtype = media.split("/")
|
||||
except ValueError:
|
||||
invalid = True
|
||||
|
||||
if invalid or not type_ or not subtype:
|
||||
raise InvalidHeader(f"Header contains invalid Accept value: {raw}")
|
||||
|
||||
params = dict(
|
||||
[
|
||||
(key.strip(), value.strip())
|
||||
for key, value in (param.split("=", 1) for param in raw_params)
|
||||
]
|
||||
)
|
||||
|
||||
return cls(mtype, MediaType(type_), MediaType(subtype), **params)
|
||||
|
||||
|
||||
class AcceptContainer(list):
|
||||
def __contains__(self, o: object) -> bool:
|
||||
return any(item.match(o) for item in self)
|
||||
|
||||
def match(
|
||||
self,
|
||||
o: object,
|
||||
*,
|
||||
allow_type_wildcard: bool = True,
|
||||
allow_subtype_wildcard: bool = True,
|
||||
) -> bool:
|
||||
return any(
|
||||
item.match(
|
||||
o,
|
||||
allow_type_wildcard=allow_type_wildcard,
|
||||
allow_subtype_wildcard=allow_subtype_wildcard,
|
||||
)
|
||||
for item in self
|
||||
)
|
||||
|
||||
|
||||
def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||
"""Parse content-type and content-disposition header values.
|
||||
|
||||
@@ -102,7 +274,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||
"""Parse traditional proxy headers."""
|
||||
real_ip_header = config.REAL_IP_HEADER
|
||||
proxies_count = config.PROXIES_COUNT
|
||||
addr = real_ip_header and headers.get(real_ip_header)
|
||||
addr = real_ip_header and headers.getone(real_ip_header, None)
|
||||
if not addr and proxies_count:
|
||||
assert proxies_count > 0
|
||||
try:
|
||||
@@ -131,7 +303,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||
("port", "x-forwarded-port"),
|
||||
("path", "x-forwarded-path"),
|
||||
):
|
||||
yield key, headers.get(header)
|
||||
yield key, headers.getone(header, None)
|
||||
|
||||
return fwd_normalize(options())
|
||||
|
||||
@@ -194,3 +366,31 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
||||
ret += b"%b: %b\r\n" % h
|
||||
ret += b"\r\n"
|
||||
return ret
|
||||
|
||||
|
||||
def _sort_accept_value(accept: Accept):
|
||||
return (
|
||||
accept.qvalue,
|
||||
len(accept.params),
|
||||
accept.subtype != "*",
|
||||
accept.type_ != "*",
|
||||
)
|
||||
|
||||
|
||||
def parse_accept(accept: str) -> AcceptContainer:
|
||||
"""Parse an Accept header and order the acceptable media types in
|
||||
accorsing to RFC 7231, s. 5.3.2
|
||||
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
"""
|
||||
media_types = accept.split(",")
|
||||
accept_list: List[Accept] = []
|
||||
|
||||
for mtype in media_types:
|
||||
if not mtype:
|
||||
continue
|
||||
|
||||
accept_list.append(Accept.parse(mtype))
|
||||
|
||||
return AcceptContainer(
|
||||
sorted(accept_list, key=_sort_accept_value, reverse=True)
|
||||
)
|
||||
|
||||
@@ -144,7 +144,7 @@ def import_string(module_name, package=None):
|
||||
import a module or class by string path.
|
||||
|
||||
:module_name: str with path of module or path to import and
|
||||
instanciate a class
|
||||
instantiate a class
|
||||
:returns: a module object or one instance from class if
|
||||
module_name is a valid path to class
|
||||
|
||||
@@ -155,3 +155,17 @@ def import_string(module_name, package=None):
|
||||
if ismodule(obj):
|
||||
return obj
|
||||
return obj()
|
||||
|
||||
|
||||
class Default:
|
||||
"""
|
||||
It is used to replace `None` or `object()` as a sentinel
|
||||
that represents a default value. Sometimes we want to set
|
||||
a value to `None` so we cannot use `None` to represent the
|
||||
default value, and `object()` is hard to be typed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
_default = Default()
|
||||
|
||||
135
sanic/http.py
135
sanic/http.py
@@ -20,7 +20,8 @@ from sanic.exceptions import (
|
||||
)
|
||||
from sanic.headers import format_http1_response
|
||||
from sanic.helpers import has_message_body
|
||||
from sanic.log import access_logger, logger
|
||||
from sanic.log import access_logger, error_logger, logger
|
||||
from sanic.touchup import TouchUpMeta
|
||||
|
||||
|
||||
class Stage(Enum):
|
||||
@@ -45,7 +46,7 @@ class Stage(Enum):
|
||||
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
||||
|
||||
|
||||
class Http:
|
||||
class Http(metaclass=TouchUpMeta):
|
||||
"""
|
||||
Internal helper for managing the HTTP request/response cycle
|
||||
|
||||
@@ -64,9 +65,18 @@ class Http:
|
||||
:raises RuntimeError:
|
||||
"""
|
||||
|
||||
HEADER_CEILING = 16_384
|
||||
HEADER_MAX_SIZE = 0
|
||||
|
||||
__touchup__ = (
|
||||
"http1_request_header",
|
||||
"http1_response_header",
|
||||
"read",
|
||||
)
|
||||
__slots__ = [
|
||||
"_send",
|
||||
"_receive_more",
|
||||
"dispatch",
|
||||
"recv_buffer",
|
||||
"protocol",
|
||||
"expecting_continue",
|
||||
@@ -82,6 +92,7 @@ class Http:
|
||||
"request_max_size",
|
||||
"response",
|
||||
"response_func",
|
||||
"response_size",
|
||||
"response_bytes_left",
|
||||
"upgrade_websocket",
|
||||
]
|
||||
@@ -91,19 +102,23 @@ class Http:
|
||||
self._receive_more = protocol.receive_more
|
||||
self.recv_buffer = protocol.recv_buffer
|
||||
self.protocol = protocol
|
||||
self.expecting_continue: bool = False
|
||||
self.keep_alive = True
|
||||
self.stage: Stage = Stage.IDLE
|
||||
self.dispatch = self.protocol.app.dispatch
|
||||
|
||||
def init_for_request(self):
|
||||
"""Init/reset all per-request variables."""
|
||||
self.exception = None
|
||||
self.expecting_continue: bool = False
|
||||
self.head_only = None
|
||||
self.request_body = None
|
||||
self.request_bytes = None
|
||||
self.request_bytes_left = None
|
||||
self.request_max_size = protocol.request_max_size
|
||||
self.keep_alive = True
|
||||
self.head_only = None
|
||||
self.request_max_size = self.protocol.request_max_size
|
||||
self.request: Request = None
|
||||
self.response: BaseHTTPResponse = None
|
||||
self.exception = None
|
||||
self.url = None
|
||||
self.upgrade_websocket = False
|
||||
self.url = None
|
||||
|
||||
def __bool__(self):
|
||||
"""Test if request handling is in progress"""
|
||||
@@ -113,14 +128,20 @@ class Http:
|
||||
"""
|
||||
HTTP 1.1 connection handler
|
||||
"""
|
||||
while True: # As long as connection stays keep-alive
|
||||
# Handle requests while the connection stays reusable
|
||||
while self.keep_alive and self.stage is Stage.IDLE:
|
||||
self.init_for_request()
|
||||
# Wait for incoming bytes (in IDLE stage)
|
||||
if not self.recv_buffer:
|
||||
await self._receive_more()
|
||||
self.stage = Stage.REQUEST
|
||||
try:
|
||||
# Receive and handle a request
|
||||
self.stage = Stage.REQUEST
|
||||
self.response_func = self.http1_response_header
|
||||
|
||||
await self.http1_request_header()
|
||||
|
||||
self.stage = Stage.HANDLER
|
||||
self.request.conn_info = self.protocol.conn_info
|
||||
await self.protocol.request_handler(self.request)
|
||||
|
||||
@@ -132,6 +153,12 @@ class Http:
|
||||
await self.response.send(end_stream=True)
|
||||
except CancelledError:
|
||||
# Write an appropriate response before exiting
|
||||
if not self.protocol.transport:
|
||||
logger.info(
|
||||
f"Request: {self.request.method} {self.request.url} "
|
||||
"stopped. Transport is closed."
|
||||
)
|
||||
return
|
||||
e = self.exception or ServiceUnavailable("Cancelled")
|
||||
self.exception = None
|
||||
self.keep_alive = False
|
||||
@@ -143,8 +170,11 @@ class Http:
|
||||
# Try to consume any remaining request body
|
||||
if self.request_body:
|
||||
if self.response and 200 <= self.response.status < 300:
|
||||
logger.error(f"{self.request} body not consumed.")
|
||||
|
||||
error_logger.error(f"{self.request} body not consumed.")
|
||||
# Limit the size because the handler may have set it infinite
|
||||
self.request_max_size = min(
|
||||
self.request_max_size, self.protocol.request_max_size
|
||||
)
|
||||
try:
|
||||
async for _ in self:
|
||||
pass
|
||||
@@ -156,19 +186,16 @@ class Http:
|
||||
await sleep(0.001)
|
||||
self.keep_alive = False
|
||||
|
||||
# Exit and disconnect if no more requests can be taken
|
||||
if self.stage is not Stage.IDLE or not self.keep_alive:
|
||||
break
|
||||
# Clean up to free memory and for the next request
|
||||
if self.request:
|
||||
self.request.stream = None
|
||||
if self.response:
|
||||
self.response.stream = None
|
||||
|
||||
# Wait for next request
|
||||
if not self.recv_buffer:
|
||||
await self._receive_more()
|
||||
|
||||
async def http1_request_header(self):
|
||||
async def http1_request_header(self): # no cov
|
||||
"""
|
||||
Receive and parse request header into self.request.
|
||||
"""
|
||||
HEADER_MAX_SIZE = min(8192, self.request_max_size)
|
||||
# Receive until full header is in buffer
|
||||
buf = self.recv_buffer
|
||||
pos = 0
|
||||
@@ -179,12 +206,12 @@ class Http:
|
||||
break
|
||||
|
||||
pos = max(0, len(buf) - 3)
|
||||
if pos >= HEADER_MAX_SIZE:
|
||||
if pos >= self.HEADER_MAX_SIZE:
|
||||
break
|
||||
|
||||
await self._receive_more()
|
||||
|
||||
if pos >= HEADER_MAX_SIZE:
|
||||
if pos >= self.HEADER_MAX_SIZE:
|
||||
raise PayloadTooLarge("Request header exceeds the size limit")
|
||||
|
||||
# Parse header content
|
||||
@@ -194,6 +221,12 @@ class Http:
|
||||
reqline, *split_headers = raw_headers.split("\r\n")
|
||||
method, self.url, protocol = reqline.split(" ")
|
||||
|
||||
await self.dispatch(
|
||||
"http.lifecycle.read_head",
|
||||
inline=True,
|
||||
context={"head": bytes(head)},
|
||||
)
|
||||
|
||||
if protocol == "HTTP/1.1":
|
||||
self.keep_alive = True
|
||||
elif protocol == "HTTP/1.0":
|
||||
@@ -218,7 +251,9 @@ class Http:
|
||||
raise InvalidUsage("Bad Request")
|
||||
|
||||
headers_instance = Header(headers)
|
||||
self.upgrade_websocket = headers_instance.get("upgrade") == "websocket"
|
||||
self.upgrade_websocket = (
|
||||
headers_instance.getone("upgrade", "").lower() == "websocket"
|
||||
)
|
||||
|
||||
# Prepare a Request object
|
||||
request = self.protocol.request_class(
|
||||
@@ -230,12 +265,17 @@ class Http:
|
||||
transport=self.protocol.transport,
|
||||
app=self.protocol.app,
|
||||
)
|
||||
await self.dispatch(
|
||||
"http.lifecycle.request",
|
||||
inline=True,
|
||||
context={"request": request},
|
||||
)
|
||||
|
||||
# Prepare for request body
|
||||
self.request_bytes_left = self.request_bytes = 0
|
||||
if request_body:
|
||||
headers = request.headers
|
||||
expect = headers.get("expect")
|
||||
expect = headers.getone("expect", None)
|
||||
|
||||
if expect is not None:
|
||||
if expect.lower() == "100-continue":
|
||||
@@ -243,7 +283,7 @@ class Http:
|
||||
else:
|
||||
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
||||
|
||||
if headers.get("transfer-encoding") == "chunked":
|
||||
if headers.getone("transfer-encoding", None) == "chunked":
|
||||
self.request_body = "chunked"
|
||||
pos -= 2 # One CRLF stays in buffer
|
||||
else:
|
||||
@@ -254,13 +294,12 @@ class Http:
|
||||
|
||||
# Remove header and its trailing CRLF
|
||||
del buf[: pos + 4]
|
||||
self.stage = Stage.HANDLER
|
||||
self.request, request.stream = request, self
|
||||
self.protocol.state["requests_count"] += 1
|
||||
|
||||
async def http1_response_header(
|
||||
self, data: bytes, end_stream: bool
|
||||
) -> None:
|
||||
) -> None: # no cov
|
||||
res = self.response
|
||||
|
||||
# Compatibility with simple response body
|
||||
@@ -270,6 +309,7 @@ class Http:
|
||||
size = len(data)
|
||||
headers = res.headers
|
||||
status = res.status
|
||||
self.response_size = size
|
||||
|
||||
if not isinstance(status, int) or status < 200:
|
||||
raise RuntimeError(f"Invalid response status {status!r}")
|
||||
@@ -424,13 +464,15 @@ class Http:
|
||||
req, res = self.request, self.response
|
||||
extra = {
|
||||
"status": getattr(res, "status", 0),
|
||||
"byte": getattr(self, "response_bytes_left", -1),
|
||||
"byte": getattr(
|
||||
self, "response_bytes_left", getattr(self, "response_size", -1)
|
||||
),
|
||||
"host": "UNKNOWN",
|
||||
"request": "nil",
|
||||
}
|
||||
if req is not None:
|
||||
if req.ip:
|
||||
extra["host"] = f"{req.ip}:{req.port}"
|
||||
if req.remote_addr or req.ip:
|
||||
extra["host"] = f"{req.remote_addr or req.ip}:{req.port}"
|
||||
extra["request"] = f"{req.method} {req.url}"
|
||||
access_logger.info("", extra=extra)
|
||||
|
||||
@@ -446,7 +488,7 @@ class Http:
|
||||
if data:
|
||||
yield data
|
||||
|
||||
async def read(self) -> Optional[bytes]:
|
||||
async def read(self) -> Optional[bytes]: # no cov
|
||||
"""
|
||||
Read some bytes of request body.
|
||||
"""
|
||||
@@ -478,8 +520,6 @@ class Http:
|
||||
self.keep_alive = False
|
||||
raise InvalidUsage("Bad chunked encoding")
|
||||
|
||||
del buf[: pos + 2]
|
||||
|
||||
if size <= 0:
|
||||
self.request_body = None
|
||||
|
||||
@@ -487,8 +527,17 @@ class Http:
|
||||
self.keep_alive = False
|
||||
raise InvalidUsage("Bad chunked encoding")
|
||||
|
||||
# Consume CRLF, chunk size 0 and the two CRLF that follow
|
||||
pos += 4
|
||||
# Might need to wait for the final CRLF
|
||||
while len(buf) < pos:
|
||||
await self._receive_more()
|
||||
del buf[:pos]
|
||||
return None
|
||||
|
||||
# Remove CRLF, chunk size and the CRLF that follows
|
||||
del buf[: pos + 2]
|
||||
|
||||
self.request_bytes_left = size
|
||||
self.request_bytes += size
|
||||
|
||||
@@ -513,6 +562,12 @@ class Http:
|
||||
|
||||
self.request_bytes_left -= size
|
||||
|
||||
await self.dispatch(
|
||||
"http.lifecycle.read_body",
|
||||
inline=True,
|
||||
context={"body": data},
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
# Response methods
|
||||
@@ -529,9 +584,21 @@ class Http:
|
||||
self.stage = Stage.FAILED
|
||||
raise RuntimeError("Response already started")
|
||||
|
||||
# Disconnect any earlier but unused response object
|
||||
if self.response is not None:
|
||||
self.response.stream = None
|
||||
|
||||
# Connect and return the response
|
||||
self.response, response.stream = response, self
|
||||
return response
|
||||
|
||||
@property
|
||||
def send(self):
|
||||
return self.response_func
|
||||
|
||||
@classmethod
|
||||
def set_header_max_size(cls, *sizes: int):
|
||||
cls.HEADER_MAX_SIZE = min(
|
||||
*sizes,
|
||||
cls.HEADER_CEILING,
|
||||
)
|
||||
|
||||
13
sanic/log.py
13
sanic/log.py
@@ -1,8 +1,11 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
|
||||
LOGGING_CONFIG_DEFAULTS = dict(
|
||||
|
||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
loggers={
|
||||
@@ -53,6 +56,14 @@ LOGGING_CONFIG_DEFAULTS = dict(
|
||||
)
|
||||
|
||||
|
||||
class Colors(str, Enum):
|
||||
END = "\033[0m"
|
||||
BLUE = "\033[01;34m"
|
||||
GREEN = "\033[01;32m"
|
||||
YELLOW = "\033[01;33m"
|
||||
RED = "\033[01;31m"
|
||||
|
||||
|
||||
logger = logging.getLogger("sanic.root")
|
||||
"""
|
||||
General Sanic logger
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
from enum import Enum, auto
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Coroutine, List, Optional, Union
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from sanic.models.futures import FutureListener
|
||||
from sanic.models.handler_types import ListenerType, Sanic
|
||||
|
||||
|
||||
class ListenerEvent(str, Enum):
|
||||
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
||||
return name.lower()
|
||||
|
||||
BEFORE_SERVER_START = auto()
|
||||
AFTER_SERVER_START = auto()
|
||||
BEFORE_SERVER_STOP = auto()
|
||||
AFTER_SERVER_STOP = auto()
|
||||
BEFORE_SERVER_START = "server.init.before"
|
||||
AFTER_SERVER_START = "server.init.after"
|
||||
BEFORE_SERVER_STOP = "server.shutdown.before"
|
||||
AFTER_SERVER_STOP = "server.shutdown.after"
|
||||
MAIN_PROCESS_START = auto()
|
||||
MAIN_PROCESS_STOP = auto()
|
||||
|
||||
@@ -26,16 +27,14 @@ class ListenerMixin:
|
||||
|
||||
def listener(
|
||||
self,
|
||||
listener_or_event: Union[
|
||||
Callable[..., Coroutine[Any, Any, None]], str
|
||||
],
|
||||
listener_or_event: Union[ListenerType[Sanic], str],
|
||||
event_or_none: Optional[str] = None,
|
||||
apply: bool = True,
|
||||
):
|
||||
) -> ListenerType[Sanic]:
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
|
||||
To be used as a deocrator:
|
||||
To be used as a decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -63,20 +62,32 @@ class ListenerMixin:
|
||||
else:
|
||||
return partial(register_listener, event=listener_or_event)
|
||||
|
||||
def main_process_start(self, listener):
|
||||
def main_process_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "main_process_start")
|
||||
|
||||
def main_process_stop(self, listener):
|
||||
def main_process_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "main_process_stop")
|
||||
|
||||
def before_server_start(self, listener):
|
||||
def before_server_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "before_server_start")
|
||||
|
||||
def after_server_start(self, listener):
|
||||
def after_server_start(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "after_server_start")
|
||||
|
||||
def before_server_stop(self, listener):
|
||||
def before_server_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "before_server_stop")
|
||||
|
||||
def after_server_stop(self, listener):
|
||||
def after_server_stop(
|
||||
self, listener: ListenerType[Sanic]
|
||||
) -> ListenerType[Sanic]:
|
||||
return self.listener(listener, "after_server_stop")
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
from ast import NodeVisitor, Return, parse
|
||||
from functools import partial, wraps
|
||||
from inspect import signature
|
||||
from inspect import getsource, signature
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from pathlib import PurePath
|
||||
from re import sub
|
||||
from textwrap import dedent
|
||||
from time import gmtime, strftime
|
||||
from typing import Iterable, List, Optional, Set, Union
|
||||
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.compat import stat_async
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
||||
from sanic.errorpages import RESPONSE_MAPPING
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
FileNotFound,
|
||||
@@ -21,15 +24,22 @@ from sanic.exceptions import (
|
||||
from sanic.handlers import ContentRangeHandler
|
||||
from sanic.log import error_logger
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
from sanic.response import HTTPResponse, file, file_stream
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
RouteWrapper = Callable[
|
||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||
]
|
||||
|
||||
|
||||
class RouteMixin:
|
||||
name: str
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._future_routes: Set[FutureRoute] = set()
|
||||
self._future_statics: Set[FutureStatic] = set()
|
||||
self.name = ""
|
||||
self.strict_slashes: Optional[bool] = False
|
||||
|
||||
def _apply_route(self, route: FutureRoute) -> List[Route]:
|
||||
@@ -42,10 +52,10 @@ class RouteMixin:
|
||||
self,
|
||||
uri: str,
|
||||
methods: Optional[Iterable[str]] = None,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
stream: bool = False,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
ignore_body: bool = False,
|
||||
apply: bool = True,
|
||||
@@ -53,7 +63,9 @@ class RouteMixin:
|
||||
websocket: bool = False,
|
||||
unquote: bool = False,
|
||||
static: bool = False,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Decorate a function to be registered as a route
|
||||
|
||||
@@ -66,6 +78,8 @@ class RouteMixin:
|
||||
:param name: user defined route name for url_for
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests)
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: tuple of routes, decorated function
|
||||
"""
|
||||
|
||||
@@ -92,6 +106,8 @@ class RouteMixin:
|
||||
nonlocal subprotocols
|
||||
nonlocal websocket
|
||||
nonlocal static
|
||||
nonlocal version_prefix
|
||||
nonlocal error_format
|
||||
|
||||
if isinstance(handler, tuple):
|
||||
# if a handler fn is already wrapped in a route, the handler
|
||||
@@ -110,10 +126,16 @@ class RouteMixin:
|
||||
"Expected either string or Iterable of host strings, "
|
||||
"not %s" % host
|
||||
)
|
||||
|
||||
if isinstance(subprotocols, (list, tuple, set)):
|
||||
if isinstance(subprotocols, list):
|
||||
# Ordered subprotocols, maintain order
|
||||
subprotocols = tuple(subprotocols)
|
||||
elif isinstance(subprotocols, set):
|
||||
# subprotocol is unordered, keep it unordered
|
||||
subprotocols = frozenset(subprotocols)
|
||||
|
||||
if not error_format or error_format == "auto":
|
||||
error_format = self._determine_error_format(handler)
|
||||
|
||||
route = FutureRoute(
|
||||
handler,
|
||||
uri,
|
||||
@@ -128,6 +150,8 @@ class RouteMixin:
|
||||
subprotocols,
|
||||
unquote,
|
||||
static,
|
||||
version_prefix,
|
||||
error_format,
|
||||
)
|
||||
|
||||
self._future_routes.add(route)
|
||||
@@ -154,21 +178,25 @@ class RouteMixin:
|
||||
if apply:
|
||||
self._apply_route(route)
|
||||
|
||||
return route, handler
|
||||
if static:
|
||||
return route, handler
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_route(
|
||||
self,
|
||||
handler,
|
||||
handler: RouteHandler,
|
||||
uri: str,
|
||||
methods: Iterable[str] = frozenset({"GET"}),
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
stream: bool = False,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteHandler:
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
@@ -182,6 +210,8 @@ class RouteMixin:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:param stream: boolean specifying if the handler is a stream handler
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
@@ -189,7 +219,8 @@ class RouteMixin:
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
_handler = getattr(handler.view_class, method.lower(), None)
|
||||
view_class = getattr(handler, "view_class")
|
||||
_handler = getattr(view_class, method.lower(), None)
|
||||
if _handler:
|
||||
methods.add(method)
|
||||
if hasattr(_handler, "is_stream"):
|
||||
@@ -214,6 +245,8 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)(handler)
|
||||
return handler
|
||||
|
||||
@@ -221,12 +254,14 @@ class RouteMixin:
|
||||
def get(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
ignore_body: bool = True,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **GET** *HTTP* method
|
||||
|
||||
@@ -236,6 +271,8 @@ class RouteMixin:
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -246,17 +283,21 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def post(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
stream: bool = False,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **POST** *HTTP* method
|
||||
|
||||
@@ -266,6 +307,8 @@ class RouteMixin:
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -276,17 +319,21 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def put(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
stream: bool = False,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **PUT** *HTTP* method
|
||||
|
||||
@@ -296,6 +343,8 @@ class RouteMixin:
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -306,17 +355,21 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def head(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
ignore_body: bool = True,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **HEAD** *HTTP* method
|
||||
|
||||
@@ -334,6 +387,8 @@ class RouteMixin:
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests), defaults to True
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -344,17 +399,21 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def options(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
ignore_body: bool = True,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **OPTIONS** *HTTP* method
|
||||
|
||||
@@ -372,6 +431,8 @@ class RouteMixin:
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests), defaults to True
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -382,17 +443,21 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def patch(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
stream=False,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **PATCH** *HTTP* method
|
||||
|
||||
@@ -412,6 +477,8 @@ class RouteMixin:
|
||||
:param ignore_body: whether the handler should ignore request
|
||||
body (eg. GET requests), defaults to True
|
||||
:type ignore_body: bool, optional
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -422,17 +489,21 @@ class RouteMixin:
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
ignore_body: bool = True,
|
||||
):
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> RouteWrapper:
|
||||
"""
|
||||
Add an API URL under the **DELETE** *HTTP* method
|
||||
|
||||
@@ -442,6 +513,8 @@ class RouteMixin:
|
||||
URLs need to terminate with a */*
|
||||
:param version: API Version
|
||||
:param name: Unique name that can be used to identify the Route
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Object decorated with :func:`route` method
|
||||
"""
|
||||
return self.route(
|
||||
@@ -452,17 +525,21 @@ class RouteMixin:
|
||||
version=version,
|
||||
name=name,
|
||||
ignore_body=ignore_body,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def websocket(
|
||||
self,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
subprotocols: Optional[List[str]] = None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
apply: bool = True,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Decorate a function to be registered as a websocket route
|
||||
@@ -474,6 +551,8 @@ class RouteMixin:
|
||||
:param subprotocols: optional list of str with supported subprotocols
|
||||
:param name: A unique name assigned to the URL so that it can
|
||||
be used with :func:`url_for`
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: tuple of routes, decorated function
|
||||
"""
|
||||
return self.route(
|
||||
@@ -486,17 +565,21 @@ class RouteMixin:
|
||||
apply=apply,
|
||||
subprotocols=subprotocols,
|
||||
websocket=True,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)
|
||||
|
||||
def add_websocket_route(
|
||||
self,
|
||||
handler,
|
||||
uri: str,
|
||||
host: Optional[str] = None,
|
||||
host: Optional[Union[str, List[str]]] = None,
|
||||
strict_slashes: Optional[bool] = None,
|
||||
subprotocols=None,
|
||||
version: Optional[int] = None,
|
||||
version: Optional[Union[int, str, float]] = None,
|
||||
name: Optional[str] = None,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
A helper method to register a function as a websocket route.
|
||||
@@ -513,6 +596,8 @@ class RouteMixin:
|
||||
handshake
|
||||
:param name: A unique name assigned to the URL so that it can
|
||||
be used with :func:`url_for`
|
||||
:param version_prefix: URL path that should be before the version
|
||||
value; default: ``/v``
|
||||
:return: Objected decorated by :func:`websocket`
|
||||
"""
|
||||
return self.websocket(
|
||||
@@ -522,6 +607,8 @@ class RouteMixin:
|
||||
subprotocols=subprotocols,
|
||||
version=version,
|
||||
name=name,
|
||||
version_prefix=version_prefix,
|
||||
error_format=error_format,
|
||||
)(handler)
|
||||
|
||||
def static(
|
||||
@@ -537,6 +624,7 @@ class RouteMixin:
|
||||
strict_slashes=None,
|
||||
content_type=None,
|
||||
apply=True,
|
||||
resource_type=None,
|
||||
):
|
||||
"""
|
||||
Register a root to serve files from. The input can either be a
|
||||
@@ -586,6 +674,7 @@ class RouteMixin:
|
||||
host,
|
||||
strict_slashes,
|
||||
content_type,
|
||||
resource_type,
|
||||
)
|
||||
self._future_statics.add(static)
|
||||
|
||||
@@ -665,7 +754,10 @@ class RouteMixin:
|
||||
modified_since = strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
||||
)
|
||||
if request.headers.get("If-Modified-Since") == modified_since:
|
||||
if (
|
||||
request.headers.getone("if-modified-since", None)
|
||||
== modified_since
|
||||
):
|
||||
return HTTPResponse(status=304)
|
||||
headers["Last-Modified"] = modified_since
|
||||
_range = None
|
||||
@@ -718,16 +810,19 @@ class RouteMixin:
|
||||
return await file(file_path, headers=headers, _range=_range)
|
||||
except ContentRangeError:
|
||||
raise
|
||||
except Exception:
|
||||
error_logger.exception(
|
||||
f"File not found: path={file_or_directory}, "
|
||||
f"relative_url={__file_uri__}"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise FileNotFound(
|
||||
"File not found",
|
||||
path=file_or_directory,
|
||||
relative_url=__file_uri__,
|
||||
)
|
||||
except Exception:
|
||||
error_logger.exception(
|
||||
f"Exception in static request handler: "
|
||||
f"path={file_or_directory}, "
|
||||
f"relative_url={__file_uri__}"
|
||||
)
|
||||
raise
|
||||
|
||||
def _register_static(
|
||||
self,
|
||||
@@ -775,8 +870,27 @@ class RouteMixin:
|
||||
name = static.name
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
if not static.resource_type:
|
||||
if not path.isfile(file_or_directory):
|
||||
uri += "/<__file_uri__:path>"
|
||||
elif static.resource_type == "dir":
|
||||
if path.isfile(file_or_directory):
|
||||
raise TypeError(
|
||||
"Resource type improperly identified as directory. "
|
||||
f"'{file_or_directory}'"
|
||||
)
|
||||
uri += "/<__file_uri__:path>"
|
||||
elif static.resource_type == "file" and not path.isfile(
|
||||
file_or_directory
|
||||
):
|
||||
raise TypeError(
|
||||
"Resource type improperly identified as file. "
|
||||
f"'{file_or_directory}'"
|
||||
)
|
||||
elif static.resource_type != "file":
|
||||
raise ValueError(
|
||||
"The resource_type should be set to 'file' or 'dir'"
|
||||
)
|
||||
|
||||
# special prefix for static files
|
||||
# if not static.name.startswith("_static_"):
|
||||
@@ -793,7 +907,7 @@ class RouteMixin:
|
||||
)
|
||||
)
|
||||
|
||||
route, _ = self.route(
|
||||
route, _ = self.route( # type: ignore
|
||||
uri=uri,
|
||||
methods=["GET", "HEAD"],
|
||||
name=name,
|
||||
@@ -803,3 +917,43 @@ class RouteMixin:
|
||||
)(_handler)
|
||||
|
||||
return route
|
||||
|
||||
def _determine_error_format(self, handler) -> Optional[str]:
|
||||
if not isinstance(handler, CompositionView):
|
||||
try:
|
||||
src = dedent(getsource(handler))
|
||||
tree = parse(src)
|
||||
http_response_types = self._get_response_types(tree)
|
||||
|
||||
if len(http_response_types) == 1:
|
||||
return next(iter(http_response_types))
|
||||
except (OSError, TypeError):
|
||||
...
|
||||
|
||||
return None
|
||||
|
||||
def _get_response_types(self, node):
|
||||
types = set()
|
||||
|
||||
class HttpResponseVisitor(NodeVisitor):
|
||||
def visit_Return(self, node: Return) -> Any:
|
||||
nonlocal types
|
||||
|
||||
try:
|
||||
checks = [node.value.func.id] # type: ignore
|
||||
if node.value.keywords: # type: ignore
|
||||
checks += [
|
||||
k.value
|
||||
for k in node.value.keywords # type: ignore
|
||||
if k.arg == "content_type"
|
||||
]
|
||||
|
||||
for check in checks:
|
||||
if check in RESPONSE_MAPPING:
|
||||
types.add(RESPONSE_MAPPING[check])
|
||||
except AttributeError:
|
||||
...
|
||||
|
||||
HttpResponseVisitor().visit(node)
|
||||
|
||||
return types
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Callable, Dict, Set
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Optional, Set, Union
|
||||
|
||||
from sanic.models.futures import FutureSignal
|
||||
from sanic.models.handler_types import SignalHandler
|
||||
@@ -19,11 +20,11 @@ class SignalMixin:
|
||||
|
||||
def signal(
|
||||
self,
|
||||
event: str,
|
||||
event: Union[str, Enum],
|
||||
*,
|
||||
apply: bool = True,
|
||||
condition: Dict[str, Any] = None,
|
||||
) -> Callable[[SignalHandler], FutureSignal]:
|
||||
) -> Callable[[SignalHandler], SignalHandler]:
|
||||
"""
|
||||
For creating a signal handler, used similar to a route handler:
|
||||
|
||||
@@ -41,29 +42,33 @@ class SignalMixin:
|
||||
filtering, defaults to None
|
||||
:type condition: Dict[str, Any], optional
|
||||
"""
|
||||
event_value = str(event.value) if isinstance(event, Enum) else event
|
||||
|
||||
def decorator(handler: SignalHandler):
|
||||
nonlocal event
|
||||
nonlocal apply
|
||||
|
||||
future_signal = FutureSignal(
|
||||
handler, event, HashableDict(condition or {})
|
||||
handler, event_value, HashableDict(condition or {})
|
||||
)
|
||||
self._future_signals.add(future_signal)
|
||||
|
||||
if apply:
|
||||
self._apply_signal(future_signal)
|
||||
|
||||
return future_signal
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_signal(
|
||||
self,
|
||||
handler,
|
||||
handler: Optional[Callable[..., Any]],
|
||||
event: str,
|
||||
condition: Dict[str, Any] = None,
|
||||
):
|
||||
if not handler:
|
||||
|
||||
async def noop():
|
||||
...
|
||||
|
||||
handler = noop
|
||||
self.signal(event=event, condition=condition)(handler)
|
||||
return handler
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.websocket import WebSocketConnection
|
||||
from sanic.server.websockets.connection import WebSocketConnection
|
||||
|
||||
|
||||
ASGIScope = MutableMapping[str, Any]
|
||||
@@ -14,10 +15,20 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
|
||||
|
||||
class MockProtocol:
|
||||
def __init__(self, transport: "MockTransport", loop):
|
||||
# This should be refactored when < 3.8 support is dropped
|
||||
self.transport = transport
|
||||
self._not_paused = asyncio.Event(loop=loop)
|
||||
self._not_paused.set()
|
||||
self._complete = asyncio.Event(loop=loop)
|
||||
# Fixup for 3.8+; Sanic still supports 3.7 where loop is required
|
||||
loop = loop if sys.version_info[:2] < (3, 8) else None
|
||||
# Optional in 3.9, necessary in 3.10 because the parameter "loop"
|
||||
# was completely removed
|
||||
if not loop:
|
||||
self._not_paused = asyncio.Event()
|
||||
self._not_paused.set()
|
||||
self._complete = asyncio.Event()
|
||||
else:
|
||||
self._not_paused = asyncio.Event(loop=loop)
|
||||
self._not_paused.set()
|
||||
self._complete = asyncio.Event(loop=loop)
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
self._not_paused.clear()
|
||||
|
||||
@@ -13,7 +13,7 @@ class FutureRoute(NamedTuple):
|
||||
handler: str
|
||||
uri: str
|
||||
methods: Optional[Iterable[str]]
|
||||
host: str
|
||||
host: Union[str, List[str]]
|
||||
strict_slashes: bool
|
||||
stream: bool
|
||||
version: Optional[int]
|
||||
@@ -23,6 +23,8 @@ class FutureRoute(NamedTuple):
|
||||
subprotocols: Optional[List[str]]
|
||||
unquote: bool
|
||||
static: bool
|
||||
version_prefix: str
|
||||
error_format: Optional[str]
|
||||
|
||||
|
||||
class FutureListener(NamedTuple):
|
||||
@@ -51,9 +53,14 @@ class FutureStatic(NamedTuple):
|
||||
host: Optional[str]
|
||||
strict_slashes: Optional[bool]
|
||||
content_type: Optional[bool]
|
||||
resource_type: Optional[str]
|
||||
|
||||
|
||||
class FutureSignal(NamedTuple):
|
||||
handler: SignalHandler
|
||||
event: str
|
||||
condition: Optional[Dict[str, str]]
|
||||
|
||||
|
||||
class FutureRegistry(set):
|
||||
...
|
||||
|
||||
@@ -21,5 +21,5 @@ MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
|
||||
ListenerType = Callable[
|
||||
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
|
||||
]
|
||||
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse]]
|
||||
RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]]
|
||||
SignalHandler = Callable[..., Coroutine[Any, Any, None]]
|
||||
|
||||
64
sanic/models/server_types.py
Normal file
64
sanic/models/server_types.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from ssl import SSLObject
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
||||
class ConnInfo:
|
||||
"""
|
||||
Local and remote addresses and SSL status info.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"client_port",
|
||||
"client",
|
||||
"client_ip",
|
||||
"ctx",
|
||||
"peername",
|
||||
"server_port",
|
||||
"server",
|
||||
"server_name",
|
||||
"sockname",
|
||||
"ssl",
|
||||
"cert",
|
||||
)
|
||||
|
||||
def __init__(self, transport: TransportProtocol, unix=None):
|
||||
self.ctx = SimpleNamespace()
|
||||
self.peername = None
|
||||
self.server = self.client = ""
|
||||
self.server_port = self.client_port = 0
|
||||
self.client_ip = ""
|
||||
self.sockname = addr = transport.get_extra_info("sockname")
|
||||
self.ssl = False
|
||||
self.server_name = ""
|
||||
self.cert: Dict[str, Any] = {}
|
||||
sslobj: Optional[SSLObject] = transport.get_extra_info(
|
||||
"ssl_object"
|
||||
) # type: ignore
|
||||
if sslobj:
|
||||
self.ssl = True
|
||||
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
|
||||
self.cert = dict(getattr(sslobj.context, "sanic", {}))
|
||||
if isinstance(addr, str): # UNIX socket
|
||||
self.server = unix or addr
|
||||
return
|
||||
|
||||
# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
|
||||
if isinstance(addr, tuple):
|
||||
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
||||
self.server_port = addr[1]
|
||||
# self.server gets non-standard port appended
|
||||
if addr[1] != (443 if self.ssl else 80):
|
||||
self.server = f"{self.server}:{addr[1]}"
|
||||
self.peername = addr = transport.get_extra_info("peername")
|
||||
|
||||
if isinstance(addr, tuple):
|
||||
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
||||
self.client_ip = addr[0]
|
||||
self.client_port = addr[1]
|
||||
@@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
@@ -46,17 +47,37 @@ def _get_args_for_reloading():
|
||||
return [sys.executable] + sys.argv
|
||||
|
||||
|
||||
def restart_with_reloader():
|
||||
def restart_with_reloader(changed=None):
|
||||
"""Create a new process and a subprocess in it with the same arguments as
|
||||
this one.
|
||||
"""
|
||||
reloaded = ",".join(changed) if changed else ""
|
||||
return subprocess.Popen(
|
||||
_get_args_for_reloading(),
|
||||
env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
|
||||
env={
|
||||
**os.environ,
|
||||
"SANIC_SERVER_RUNNING": "true",
|
||||
"SANIC_RELOADER_PROCESS": "true",
|
||||
"SANIC_RELOADED_FILES": reloaded,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def watchdog(sleep_interval):
|
||||
def _check_file(filename, mtimes):
|
||||
need_reload = False
|
||||
|
||||
mtime = os.stat(filename).st_mtime
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
elif mtime > old_time:
|
||||
mtimes[filename] = mtime
|
||||
need_reload = True
|
||||
|
||||
return need_reload
|
||||
|
||||
|
||||
def watchdog(sleep_interval, app):
|
||||
"""Watch project files, restart worker process if a change happened.
|
||||
|
||||
:param sleep_interval: interval in second.
|
||||
@@ -75,25 +96,27 @@ def watchdog(sleep_interval):
|
||||
|
||||
try:
|
||||
while True:
|
||||
need_reload = False
|
||||
|
||||
for filename in _iter_module_files():
|
||||
changed = set()
|
||||
for filename in itertools.chain(
|
||||
_iter_module_files(),
|
||||
*(d.glob("**/*") for d in app.reload_dirs),
|
||||
):
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
if _check_file(filename, mtimes):
|
||||
path = (
|
||||
filename
|
||||
if isinstance(filename, str)
|
||||
else filename.resolve()
|
||||
)
|
||||
changed.add(str(path))
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
elif mtime > old_time:
|
||||
mtimes[filename] = mtime
|
||||
need_reload = True
|
||||
|
||||
if need_reload:
|
||||
if changed:
|
||||
worker_process.terminate()
|
||||
worker_process.wait()
|
||||
worker_process = restart_with_reloader()
|
||||
worker_process = restart_with_reloader(changed)
|
||||
|
||||
sleep(sleep_interval)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -18,7 +18,6 @@ from sanic_routing.route import Route # type: ignore
|
||||
if TYPE_CHECKING:
|
||||
from sanic.server import ConnInfo
|
||||
from sanic.app import Sanic
|
||||
from sanic.http import Http
|
||||
|
||||
import email.utils
|
||||
import uuid
|
||||
@@ -32,14 +31,17 @@ from httptools import parse_url # type: ignore
|
||||
|
||||
from sanic.compat import CancelledErrors, Header
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.exceptions import InvalidUsage, ServerError
|
||||
from sanic.headers import (
|
||||
AcceptContainer,
|
||||
Options,
|
||||
parse_accept,
|
||||
parse_content_header,
|
||||
parse_forwarded,
|
||||
parse_host,
|
||||
parse_xforwarded,
|
||||
)
|
||||
from sanic.http import Http, Stage
|
||||
from sanic.log import error_logger, logger
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||
@@ -94,6 +96,7 @@ class Request:
|
||||
"head",
|
||||
"headers",
|
||||
"method",
|
||||
"parsed_accept",
|
||||
"parsed_args",
|
||||
"parsed_not_grouped_args",
|
||||
"parsed_files",
|
||||
@@ -101,6 +104,7 @@ class Request:
|
||||
"parsed_json",
|
||||
"parsed_forwarded",
|
||||
"raw_url",
|
||||
"responded",
|
||||
"request_middleware_started",
|
||||
"route",
|
||||
"stream",
|
||||
@@ -125,7 +129,7 @@ class Request:
|
||||
self._name: Optional[str] = None
|
||||
self.app = app
|
||||
|
||||
self.headers = headers
|
||||
self.headers = Header(headers)
|
||||
self.version = version
|
||||
self.method = method
|
||||
self.transport = transport
|
||||
@@ -136,6 +140,7 @@ class Request:
|
||||
self.conn_info: Optional[ConnInfo] = None
|
||||
self.ctx = SimpleNamespace()
|
||||
self.parsed_forwarded: Optional[Options] = None
|
||||
self.parsed_accept: Optional[AcceptContainer] = None
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
@@ -151,6 +156,7 @@ class Request:
|
||||
self.stream: Optional[Http] = None
|
||||
self.route: Optional[Route] = None
|
||||
self._protocol = None
|
||||
self.responded: bool = False
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
@@ -160,6 +166,21 @@ class Request:
|
||||
def generate_id(*_):
|
||||
return uuid.uuid4()
|
||||
|
||||
def reset_response(self):
|
||||
try:
|
||||
if (
|
||||
self.stream is not None
|
||||
and self.stream.stage is not Stage.HANDLER
|
||||
):
|
||||
raise ServerError(
|
||||
"Cannot reset response because previous response was sent."
|
||||
)
|
||||
self.stream.response.stream = None
|
||||
self.stream.response = None
|
||||
self.responded = False
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
async def respond(
|
||||
self,
|
||||
response: Optional[BaseHTTPResponse] = None,
|
||||
@@ -168,13 +189,19 @@ class Request:
|
||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||
content_type: Optional[str] = None,
|
||||
):
|
||||
try:
|
||||
if self.stream is not None and self.stream.response:
|
||||
raise ServerError("Second respond call is not allowed.")
|
||||
except AttributeError:
|
||||
pass
|
||||
# This logic of determining which response to use is subject to change
|
||||
if response is None:
|
||||
response = (self.stream and self.stream.response) or HTTPResponse(
|
||||
response = HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
# Connect the response
|
||||
if isinstance(response, BaseHTTPResponse) and self.stream:
|
||||
response = self.stream.respond(response)
|
||||
@@ -189,6 +216,7 @@ class Request:
|
||||
error_logger.exception(
|
||||
"Exception occurred in one of response middleware handlers"
|
||||
)
|
||||
self.responded = True
|
||||
return response
|
||||
|
||||
async def receive_body(self):
|
||||
@@ -262,7 +290,7 @@ class Request:
|
||||
app = Sanic("MyApp", request_class=IntRequest)
|
||||
"""
|
||||
if not self._id:
|
||||
self._id = self.headers.get(
|
||||
self._id = self.headers.getone(
|
||||
self.app.config.REQUEST_ID_HEADER,
|
||||
self.__class__.generate_id(self), # type: ignore
|
||||
)
|
||||
@@ -296,6 +324,13 @@ class Request:
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
@property
|
||||
def accept(self) -> AcceptContainer:
|
||||
if self.parsed_accept is None:
|
||||
accept_header = self.headers.getone("accept", "")
|
||||
self.parsed_accept = parse_accept(accept_header)
|
||||
return self.parsed_accept
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
"""Attempt to return the auth header token.
|
||||
@@ -303,7 +338,7 @@ class Request:
|
||||
:return: token related to request
|
||||
"""
|
||||
prefixes = ("Bearer", "Token")
|
||||
auth_header = self.headers.get("Authorization")
|
||||
auth_header = self.headers.getone("authorization", None)
|
||||
|
||||
if auth_header is not None:
|
||||
for prefix in prefixes:
|
||||
@@ -317,8 +352,8 @@ class Request:
|
||||
if self.parsed_form is None:
|
||||
self.parsed_form = RequestParameters()
|
||||
self.parsed_files = RequestParameters()
|
||||
content_type = self.headers.get(
|
||||
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
||||
content_type = self.headers.getone(
|
||||
"content-type", DEFAULT_HTTP_CONTENT_TYPE
|
||||
)
|
||||
content_type, parameters = parse_content_header(content_type)
|
||||
try:
|
||||
@@ -378,9 +413,12 @@ class Request:
|
||||
:type errors: str
|
||||
:return: RequestParameters
|
||||
"""
|
||||
if not self.parsed_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
]:
|
||||
if (
|
||||
keep_blank_values,
|
||||
strict_parsing,
|
||||
encoding,
|
||||
errors,
|
||||
) not in self.parsed_args:
|
||||
if self.query_string:
|
||||
self.parsed_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
@@ -434,9 +472,12 @@ class Request:
|
||||
:type errors: str
|
||||
:return: list
|
||||
"""
|
||||
if not self.parsed_not_grouped_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
]:
|
||||
if (
|
||||
keep_blank_values,
|
||||
strict_parsing,
|
||||
encoding,
|
||||
errors,
|
||||
) not in self.parsed_not_grouped_args:
|
||||
if self.query_string:
|
||||
self.parsed_not_grouped_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
@@ -465,7 +506,7 @@ class Request:
|
||||
"""
|
||||
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get("Cookie")
|
||||
cookie = self.headers.getone("cookie", None)
|
||||
if cookie is not None:
|
||||
cookies: SimpleCookie = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
@@ -482,7 +523,7 @@ class Request:
|
||||
:return: Content-Type header form the request
|
||||
:rtype: str
|
||||
"""
|
||||
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
@@ -491,6 +532,10 @@ class Request:
|
||||
"""
|
||||
return self._match_info
|
||||
|
||||
@match_info.setter
|
||||
def match_info(self, value):
|
||||
self._match_info = value
|
||||
|
||||
# Transport properties (obtained from local interface only)
|
||||
|
||||
@property
|
||||
@@ -499,7 +544,7 @@ class Request:
|
||||
:return: peer ip of the socket
|
||||
:rtype: str
|
||||
"""
|
||||
return self.conn_info.client if self.conn_info else ""
|
||||
return self.conn_info.client_ip if self.conn_info else ""
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
@@ -581,7 +626,7 @@ class Request:
|
||||
|
||||
if (
|
||||
self.app.websocket_enabled
|
||||
and self.headers.get("upgrade") == "websocket"
|
||||
and self.headers.getone("upgrade", "").lower() == "websocket"
|
||||
):
|
||||
scheme = "ws"
|
||||
else:
|
||||
@@ -608,7 +653,9 @@ class Request:
|
||||
server_name = self.app.config.get("SERVER_NAME")
|
||||
if server_name:
|
||||
return server_name.split("//", 1)[-1].split("/", 1)[0]
|
||||
return str(self.forwarded.get("host") or self.headers.get("host", ""))
|
||||
return str(
|
||||
self.forwarded.get("host") or self.headers.getone("host", "")
|
||||
)
|
||||
|
||||
@property
|
||||
def server_name(self) -> str:
|
||||
@@ -737,9 +784,10 @@ def parse_multipart_form(body, boundary):
|
||||
break
|
||||
|
||||
colon_index = form_line.index(":")
|
||||
idx = colon_index + 2
|
||||
form_header_field = form_line[0:colon_index].lower()
|
||||
form_header_value, form_parameters = parse_content_header(
|
||||
form_line[colon_index + 2 :]
|
||||
form_line[idx:]
|
||||
)
|
||||
|
||||
if form_header_field == "content-disposition":
|
||||
|
||||
@@ -3,6 +3,7 @@ from mimetypes import guess_type
|
||||
from os import path
|
||||
from pathlib import PurePath
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AnyStr,
|
||||
Callable,
|
||||
@@ -19,11 +20,15 @@ from warnings import warn
|
||||
from sanic.compat import Header, open_async
|
||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||
from sanic.cookies import CookieJar
|
||||
from sanic.exceptions import SanicException, ServerError
|
||||
from sanic.helpers import has_message_body, remove_entity_headers
|
||||
from sanic.http import Http
|
||||
from sanic.models.protocol_types import HTMLProtocol, Range
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.asgi import ASGIApp
|
||||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except ImportError:
|
||||
@@ -45,7 +50,7 @@ class BaseHTTPResponse:
|
||||
self.asgi: bool = False
|
||||
self.body: Optional[bytes] = None
|
||||
self.content_type: Optional[str] = None
|
||||
self.stream: Http = None
|
||||
self.stream: Optional[Union[Http, ASGIApp]] = None
|
||||
self.status: int = None
|
||||
self.headers = Header({})
|
||||
self._cookies: Optional[CookieJar] = None
|
||||
@@ -101,7 +106,7 @@ class BaseHTTPResponse:
|
||||
|
||||
async def send(
|
||||
self,
|
||||
data: Optional[Union[AnyStr]] = None,
|
||||
data: Optional[AnyStr] = None,
|
||||
end_stream: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -112,8 +117,17 @@ class BaseHTTPResponse:
|
||||
"""
|
||||
if data is None and end_stream is None:
|
||||
end_stream = True
|
||||
if end_stream and not data and self.stream.send is None:
|
||||
return
|
||||
if self.stream is None:
|
||||
raise SanicException(
|
||||
"No stream is connected to the response object instance."
|
||||
)
|
||||
if self.stream.send is None:
|
||||
if end_stream and not data:
|
||||
return
|
||||
raise ServerError(
|
||||
"Response stream was ended, no more response data is "
|
||||
"allowed to be sent."
|
||||
)
|
||||
data = (
|
||||
data.encode() # type: ignore
|
||||
if hasattr(data, "encode")
|
||||
@@ -143,7 +157,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
|
||||
.. warning::
|
||||
|
||||
**Deprecated** and set for removal in v21.6. You can now achieve the
|
||||
**Deprecated** and set for removal in v21.12. You can now achieve the
|
||||
same functionality without a callback.
|
||||
|
||||
.. code-block:: python
|
||||
@@ -174,12 +188,16 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
status: int = 200,
|
||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||
content_type: str = "text/plain; charset=utf-8",
|
||||
chunked="deprecated",
|
||||
ignore_deprecation_notice: bool = False,
|
||||
):
|
||||
if chunked != "deprecated":
|
||||
if not ignore_deprecation_notice:
|
||||
warn(
|
||||
"The chunked argument has been deprecated and will be "
|
||||
"removed in v21.6"
|
||||
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
|
||||
"will be removed in v21.12. Please upgrade your streaming "
|
||||
"response implementation. You can learn more here: "
|
||||
"https://sanicframework.org/en/guide/advanced/streaming.html"
|
||||
"#response-streaming. If you use the builtin stream() or "
|
||||
"file_stream() methods, this upgrade will be be done for you."
|
||||
)
|
||||
|
||||
super().__init__()
|
||||
@@ -203,6 +221,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
self.streaming_fn = None
|
||||
await super().send(*args, **kwargs)
|
||||
|
||||
async def eof(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
"""
|
||||
@@ -235,6 +256,15 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
self.headers = Header(headers or {})
|
||||
self._cookies = None
|
||||
|
||||
async def eof(self):
|
||||
await self.send("", True)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self.send
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
await self.eof()
|
||||
|
||||
|
||||
def empty(
|
||||
status=204, headers: Optional[Dict[str, str]] = None
|
||||
@@ -396,7 +426,6 @@ async def file_stream(
|
||||
mime_type: Optional[str] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
filename: Optional[str] = None,
|
||||
chunked="deprecated",
|
||||
_range: Optional[Range] = None,
|
||||
) -> StreamingHTTPResponse:
|
||||
"""Return a streaming response object with file data.
|
||||
@@ -409,12 +438,6 @@ async def file_stream(
|
||||
:param chunked: Deprecated
|
||||
:param _range:
|
||||
"""
|
||||
if chunked != "deprecated":
|
||||
warn(
|
||||
"The chunked argument has been deprecated and will be "
|
||||
"removed in v21.6"
|
||||
)
|
||||
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
@@ -453,6 +476,7 @@ async def file_stream(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
ignore_deprecation_notice=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -461,7 +485,6 @@ def stream(
|
||||
status: int = 200,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
content_type: str = "text/plain; charset=utf-8",
|
||||
chunked="deprecated",
|
||||
):
|
||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||
@@ -482,17 +505,12 @@ def stream(
|
||||
:param headers: Custom Headers.
|
||||
:param chunked: Deprecated
|
||||
"""
|
||||
if chunked != "deprecated":
|
||||
warn(
|
||||
"The chunked argument has been deprecated and will be "
|
||||
"removed in v21.6"
|
||||
)
|
||||
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
status=status,
|
||||
ignore_deprecation_notice=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from inspect import signature
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
from uuid import UUID
|
||||
|
||||
from sanic_routing import BaseRouter # type: ignore
|
||||
from sanic_routing.exceptions import NoMethod # type: ignore
|
||||
@@ -9,6 +13,7 @@ from sanic_routing.exceptions import (
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.errorpages import check_error_format
|
||||
from sanic.exceptions import MethodNotSupported, NotFound, SanicException
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
|
||||
@@ -33,7 +38,7 @@ class Router(BaseRouter):
|
||||
return self.resolve(
|
||||
path=path,
|
||||
method=method,
|
||||
extra={"host": host},
|
||||
extra={"host": host} if host else None,
|
||||
)
|
||||
except RoutingNotFound as e:
|
||||
raise NotFound("Requested URL {} not found".format(e.path))
|
||||
@@ -49,7 +54,7 @@ class Router(BaseRouter):
|
||||
self, path: str, method: str, host: Optional[str]
|
||||
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve a `Route` object containg the details about how to handle
|
||||
Retrieve a `Route` object containing the details about how to handle
|
||||
a response for a given request
|
||||
|
||||
:param request: the incoming request object
|
||||
@@ -73,6 +78,8 @@ class Router(BaseRouter):
|
||||
name: Optional[str] = None,
|
||||
unquote: bool = False,
|
||||
static: bool = False,
|
||||
version_prefix: str = "/v",
|
||||
error_format: Optional[str] = None,
|
||||
) -> Union[Route, List[Route]]:
|
||||
"""
|
||||
Add a handler to the router
|
||||
@@ -103,12 +110,14 @@ class Router(BaseRouter):
|
||||
"""
|
||||
if version is not None:
|
||||
version = str(version).strip("/").lstrip("v")
|
||||
uri = "/".join([f"/v{version}", uri.lstrip("/")])
|
||||
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
|
||||
|
||||
uri = self._normalize(uri, handler)
|
||||
|
||||
params = dict(
|
||||
path=uri,
|
||||
handler=handler,
|
||||
methods=methods,
|
||||
methods=frozenset(map(str, methods)) if methods else None,
|
||||
name=name,
|
||||
strict=strict_slashes,
|
||||
unquote=unquote,
|
||||
@@ -130,6 +139,10 @@ class Router(BaseRouter):
|
||||
route.ctx.stream = stream
|
||||
route.ctx.hosts = hosts
|
||||
route.ctx.static = static
|
||||
route.ctx.error_format = error_format
|
||||
|
||||
if error_format:
|
||||
check_error_format(route.ctx.error_format)
|
||||
|
||||
routes.append(route)
|
||||
|
||||
@@ -161,7 +174,7 @@ class Router(BaseRouter):
|
||||
|
||||
@property
|
||||
def routes_all(self):
|
||||
return self.routes
|
||||
return {route.parts: route for route in self.routes}
|
||||
|
||||
@property
|
||||
def routes_static(self):
|
||||
@@ -186,3 +199,24 @@ class Router(BaseRouter):
|
||||
raise SanicException(
|
||||
f"Invalid route: {route}. Parameter names cannot use '__'."
|
||||
)
|
||||
|
||||
def _normalize(self, uri: str, handler: RouteHandler) -> str:
|
||||
if "<" not in uri:
|
||||
return uri
|
||||
|
||||
sig = signature(handler)
|
||||
mapping = {
|
||||
param.name: param.annotation.__name__.lower()
|
||||
for param in sig.parameters.values()
|
||||
if param.annotation in (str, int, float, UUID)
|
||||
}
|
||||
|
||||
reconstruction = []
|
||||
for part in uri.split("/"):
|
||||
if part.startswith("<") and ":" not in part:
|
||||
name = part[1:-1]
|
||||
annotation = mapping.get(name)
|
||||
if annotation:
|
||||
part = f"<{name}:{annotation}>"
|
||||
reconstruction.append(part)
|
||||
return "/".join(reconstruction)
|
||||
|
||||
794
sanic/server.py
794
sanic/server.py
@@ -1,794 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ssl import SSLContext
|
||||
from types import SimpleNamespace
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from sanic.models.handler_types import ListenerType
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.app import Sanic
|
||||
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
import os
|
||||
import secrets
|
||||
import socket
|
||||
import stat
|
||||
|
||||
from asyncio import CancelledError
|
||||
from asyncio.transports import Transport
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from ipaddress import ip_address
|
||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
from signal import signal as signal_func
|
||||
from time import monotonic as current_time
|
||||
|
||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import RequestTimeout, ServiceUnavailable
|
||||
from sanic.http import Http, Stage
|
||||
from sanic.log import logger
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
from sanic.request import Request
|
||||
|
||||
|
||||
try:
|
||||
import uvloop # type: ignore
|
||||
|
||||
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
||||
class ConnInfo:
|
||||
"""
|
||||
Local and remote addresses and SSL status info.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"client_port",
|
||||
"client",
|
||||
"ctx",
|
||||
"peername",
|
||||
"server_port",
|
||||
"server",
|
||||
"sockname",
|
||||
"ssl",
|
||||
)
|
||||
|
||||
def __init__(self, transport: TransportProtocol, unix=None):
|
||||
self.ctx = SimpleNamespace()
|
||||
self.peername = None
|
||||
self.server = self.client = ""
|
||||
self.server_port = self.client_port = 0
|
||||
self.sockname = addr = transport.get_extra_info("sockname")
|
||||
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
|
||||
|
||||
if isinstance(addr, str): # UNIX socket
|
||||
self.server = unix or addr
|
||||
return
|
||||
|
||||
# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
|
||||
if isinstance(addr, tuple):
|
||||
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
||||
self.server_port = addr[1]
|
||||
# self.server gets non-standard port appended
|
||||
if addr[1] != (443 if self.ssl else 80):
|
||||
self.server = f"{self.server}:{addr[1]}"
|
||||
self.peername = addr = transport.get_extra_info("peername")
|
||||
|
||||
if isinstance(addr, tuple):
|
||||
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
||||
self.client_port = addr[1]
|
||||
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
"""
|
||||
This class provides a basic HTTP implementation of the sanic framework.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
# app
|
||||
"app",
|
||||
# event loop, connection
|
||||
"loop",
|
||||
"transport",
|
||||
"connections",
|
||||
"signal",
|
||||
"conn_info",
|
||||
"ctx",
|
||||
# request params
|
||||
"request",
|
||||
# request config
|
||||
"request_handler",
|
||||
"request_timeout",
|
||||
"response_timeout",
|
||||
"keep_alive_timeout",
|
||||
"request_max_size",
|
||||
"request_buffer_queue_size",
|
||||
"request_class",
|
||||
"error_handler",
|
||||
# enable or disable access log purpose
|
||||
"access_log",
|
||||
# connection management
|
||||
"state",
|
||||
"url",
|
||||
"_handler_task",
|
||||
"_can_write",
|
||||
"_data_received",
|
||||
"_time",
|
||||
"_task",
|
||||
"_http",
|
||||
"_exception",
|
||||
"recv_buffer",
|
||||
"_unix",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
loop,
|
||||
app: Sanic,
|
||||
signal=None,
|
||||
connections=None,
|
||||
state=None,
|
||||
unix=None,
|
||||
**kwargs,
|
||||
):
|
||||
asyncio.set_event_loop(loop)
|
||||
self.loop = loop
|
||||
self.app: Sanic = app
|
||||
self.url = None
|
||||
self.transport: Optional[Transport] = None
|
||||
self.conn_info: Optional[ConnInfo] = None
|
||||
self.request: Optional[Request] = None
|
||||
self.signal = signal or Signal()
|
||||
self.access_log = self.app.config.ACCESS_LOG
|
||||
self.connections = connections if connections is not None else set()
|
||||
self.request_handler = self.app.handle_request
|
||||
self.error_handler = self.app.error_handler
|
||||
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
||||
self.request_buffer_queue_size = (
|
||||
self.app.config.REQUEST_BUFFER_QUEUE_SIZE
|
||||
)
|
||||
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
|
||||
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
|
||||
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
|
||||
self.request_class = self.app.request_class or Request
|
||||
self.state = state if state else {}
|
||||
if "requests_count" not in self.state:
|
||||
self.state["requests_count"] = 0
|
||||
self._data_received = asyncio.Event()
|
||||
self._can_write = asyncio.Event()
|
||||
self._can_write.set()
|
||||
self._exception = None
|
||||
self._unix = unix
|
||||
|
||||
def _setup_connection(self):
|
||||
self._http = Http(self)
|
||||
self._time = current_time()
|
||||
self.check_timeouts()
|
||||
|
||||
async def connection_task(self):
|
||||
"""
|
||||
Run a HTTP connection.
|
||||
|
||||
Timeouts and some additional error handling occur here, while most of
|
||||
everything else happens in class Http or in code called from there.
|
||||
"""
|
||||
try:
|
||||
self._setup_connection()
|
||||
await self._http.http1()
|
||||
except CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("protocol.connection_task uncaught")
|
||||
finally:
|
||||
if self.app.debug and self._http:
|
||||
ip = self.transport.get_extra_info("peername")
|
||||
logger.error(
|
||||
"Connection lost before response written"
|
||||
f" @ {ip} {self._http.request}"
|
||||
)
|
||||
self._http = None
|
||||
self._task = None
|
||||
try:
|
||||
self.close()
|
||||
except BaseException:
|
||||
logger.exception("Closing failed")
|
||||
|
||||
async def receive_more(self):
|
||||
"""
|
||||
Wait until more data is received into the Server protocol's buffer
|
||||
"""
|
||||
self.transport.resume_reading()
|
||||
self._data_received.clear()
|
||||
await self._data_received.wait()
|
||||
|
||||
def check_timeouts(self):
|
||||
"""
|
||||
Runs itself periodically to enforce any expired timeouts.
|
||||
"""
|
||||
try:
|
||||
if not self._task:
|
||||
return
|
||||
duration = current_time() - self._time
|
||||
stage = self._http.stage
|
||||
if stage is Stage.IDLE and duration > self.keep_alive_timeout:
|
||||
logger.debug("KeepAlive Timeout. Closing connection.")
|
||||
elif stage is Stage.REQUEST and duration > self.request_timeout:
|
||||
logger.debug("Request Timeout. Closing connection.")
|
||||
self._http.exception = RequestTimeout("Request Timeout")
|
||||
elif stage is Stage.HANDLER and self._http.upgrade_websocket:
|
||||
logger.debug("Handling websocket. Timeouts disabled.")
|
||||
return
|
||||
elif (
|
||||
stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
|
||||
and duration > self.response_timeout
|
||||
):
|
||||
logger.debug("Response Timeout. Closing connection.")
|
||||
self._http.exception = ServiceUnavailable("Response Timeout")
|
||||
else:
|
||||
interval = (
|
||||
min(
|
||||
self.keep_alive_timeout,
|
||||
self.request_timeout,
|
||||
self.response_timeout,
|
||||
)
|
||||
/ 2
|
||||
)
|
||||
self.loop.call_later(max(0.1, interval), self.check_timeouts)
|
||||
return
|
||||
self._task.cancel()
|
||||
except Exception:
|
||||
logger.exception("protocol.check_timeouts")
|
||||
|
||||
async def send(self, data):
|
||||
"""
|
||||
Writes data with backpressure control.
|
||||
"""
|
||||
await self._can_write.wait()
|
||||
if self.transport.is_closing():
|
||||
raise CancelledError
|
||||
self.transport.write(data)
|
||||
self._time = current_time()
|
||||
|
||||
def close_if_idle(self) -> bool:
|
||||
"""
|
||||
Close the connection if a request is not being sent or received
|
||||
|
||||
:return: boolean - True if closed, false if staying open
|
||||
"""
|
||||
if self._http is None or self._http.stage is Stage.IDLE:
|
||||
self.close()
|
||||
return True
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Force close the connection.
|
||||
"""
|
||||
# Cause a call to connection_lost where further cleanup occurs
|
||||
if self.transport:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Only asyncio.Protocol callbacks below this
|
||||
# -------------------------------------------- #
|
||||
|
||||
def connection_made(self, transport):
|
||||
try:
|
||||
# TODO: Benchmark to find suitable write buffer limits
|
||||
transport.set_write_buffer_limits(low=16384, high=65536)
|
||||
self.connections.add(self)
|
||||
self.transport = transport
|
||||
self._task = self.loop.create_task(self.connection_task())
|
||||
self.recv_buffer = bytearray()
|
||||
self.conn_info = ConnInfo(self.transport, unix=self._unix)
|
||||
except Exception:
|
||||
logger.exception("protocol.connect_made")
|
||||
|
||||
def connection_lost(self, exc):
|
||||
try:
|
||||
self.connections.discard(self)
|
||||
self.resume_writing()
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
except Exception:
|
||||
logger.exception("protocol.connection_lost")
|
||||
|
||||
def pause_writing(self):
|
||||
self._can_write.clear()
|
||||
|
||||
def resume_writing(self):
|
||||
self._can_write.set()
|
||||
|
||||
def data_received(self, data: bytes):
|
||||
try:
|
||||
self._time = current_time()
|
||||
if not data:
|
||||
return self.close()
|
||||
self.recv_buffer += data
|
||||
|
||||
if (
|
||||
len(self.recv_buffer) > self.app.config.REQUEST_BUFFER_SIZE
|
||||
and self.transport
|
||||
):
|
||||
self.transport.pause_reading()
|
||||
|
||||
if self._data_received:
|
||||
self._data_received.set()
|
||||
except Exception:
|
||||
logger.exception("protocol.data_received")
|
||||
|
||||
|
||||
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
|
||||
"""
|
||||
Trigger event callbacks (functions or async)
|
||||
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
"""
|
||||
if events:
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
|
||||
class AsyncioServer:
|
||||
"""
|
||||
Wraps an asyncio server with functionality that might be useful to
|
||||
a user who needs to manage the server lifecycle manually.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"loop",
|
||||
"serve_coro",
|
||||
"_after_start",
|
||||
"_before_stop",
|
||||
"_after_stop",
|
||||
"server",
|
||||
"connections",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
loop,
|
||||
serve_coro,
|
||||
connections,
|
||||
after_start: Optional[Iterable[ListenerType]],
|
||||
before_stop: Optional[Iterable[ListenerType]],
|
||||
after_stop: Optional[Iterable[ListenerType]],
|
||||
):
|
||||
# Note, Sanic already called "before_server_start" events
|
||||
# before this helper was even created. So we don't need it here.
|
||||
self.loop = loop
|
||||
self.serve_coro = serve_coro
|
||||
self._after_start = after_start
|
||||
self._before_stop = before_stop
|
||||
self._after_stop = after_stop
|
||||
self.server = None
|
||||
self.connections = connections
|
||||
|
||||
def after_start(self):
|
||||
"""
|
||||
Trigger "after_server_start" events
|
||||
"""
|
||||
trigger_events(self._after_start, self.loop)
|
||||
|
||||
def before_stop(self):
|
||||
"""
|
||||
Trigger "before_server_stop" events
|
||||
"""
|
||||
trigger_events(self._before_stop, self.loop)
|
||||
|
||||
def after_stop(self):
|
||||
"""
|
||||
Trigger "after_server_stop" events
|
||||
"""
|
||||
trigger_events(self._after_stop, self.loop)
|
||||
|
||||
def is_serving(self) -> bool:
|
||||
if self.server:
|
||||
return self.server.is_serving()
|
||||
return False
|
||||
|
||||
def wait_closed(self):
|
||||
if self.server:
|
||||
return self.server.wait_closed()
|
||||
|
||||
def close(self):
|
||||
if self.server:
|
||||
self.server.close()
|
||||
coro = self.wait_closed()
|
||||
task = asyncio.ensure_future(coro, loop=self.loop)
|
||||
return task
|
||||
|
||||
def start_serving(self):
|
||||
if self.server:
|
||||
try:
|
||||
return self.server.start_serving()
|
||||
except AttributeError:
|
||||
raise NotImplementedError(
|
||||
"server.start_serving not available in this version "
|
||||
"of asyncio or uvloop."
|
||||
)
|
||||
|
||||
def serve_forever(self):
|
||||
if self.server:
|
||||
try:
|
||||
return self.server.serve_forever()
|
||||
except AttributeError:
|
||||
raise NotImplementedError(
|
||||
"server.serve_forever not available in this version "
|
||||
"of asyncio or uvloop."
|
||||
)
|
||||
|
||||
def __await__(self):
|
||||
"""
|
||||
Starts the asyncio server, returns AsyncServerCoro
|
||||
"""
|
||||
task = asyncio.ensure_future(self.serve_coro)
|
||||
while not task.done():
|
||||
yield
|
||||
self.server = task.result()
|
||||
return self
|
||||
|
||||
|
||||
def serve(
|
||||
host,
|
||||
port,
|
||||
app,
|
||||
before_start: Optional[Iterable[ListenerType]] = None,
|
||||
after_start: Optional[Iterable[ListenerType]] = None,
|
||||
before_stop: Optional[Iterable[ListenerType]] = None,
|
||||
after_stop: Optional[Iterable[ListenerType]] = None,
|
||||
ssl: Optional[SSLContext] = None,
|
||||
sock: Optional[socket.socket] = None,
|
||||
unix: Optional[str] = None,
|
||||
reuse_port: bool = False,
|
||||
loop=None,
|
||||
protocol: Type[asyncio.Protocol] = HttpProtocol,
|
||||
backlog: int = 100,
|
||||
register_sys_signals: bool = True,
|
||||
run_multiple: bool = False,
|
||||
run_async: bool = False,
|
||||
connections=None,
|
||||
signal=Signal(),
|
||||
state=None,
|
||||
asyncio_server_kwargs=None,
|
||||
):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param before_start: function to be executed before the server starts
|
||||
listening. Takes arguments `app` instance and `loop`
|
||||
:param after_start: function to be executed after the server starts
|
||||
listening. Takes arguments `app` instance and `loop`
|
||||
:param before_stop: function to be executed when a stop signal is
|
||||
received before it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
:param after_stop: function to be executed when a stop signal is
|
||||
received after it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param unix: Unix socket to listen on instead of TCP port
|
||||
:param reuse_port: `True` for multiple workers
|
||||
:param loop: asyncio compatible event loop
|
||||
:param run_async: bool: Do not create a new event loop for the server,
|
||||
and return an AsyncServer object rather than running it
|
||||
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
|
||||
create_server method
|
||||
:return: Nothing
|
||||
"""
|
||||
if not run_async and not loop:
|
||||
# create new event_loop after fork
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
if app.debug:
|
||||
loop.set_debug(app.debug)
|
||||
|
||||
app.asgi = False
|
||||
|
||||
connections = connections if connections is not None else set()
|
||||
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
connections=connections,
|
||||
signal=signal,
|
||||
app=app,
|
||||
state=state,
|
||||
unix=unix,
|
||||
**protocol_kwargs,
|
||||
)
|
||||
asyncio_server_kwargs = (
|
||||
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||
)
|
||||
# UNIX sockets are always bound by us (to preserve semantics between modes)
|
||||
if unix:
|
||||
sock = bind_unix_socket(unix, backlog=backlog)
|
||||
server_coroutine = loop.create_server(
|
||||
server,
|
||||
None if sock else host,
|
||||
None if sock else port,
|
||||
ssl=ssl,
|
||||
reuse_port=reuse_port,
|
||||
sock=sock,
|
||||
backlog=backlog,
|
||||
**asyncio_server_kwargs,
|
||||
)
|
||||
|
||||
if run_async:
|
||||
return AsyncioServer(
|
||||
loop=loop,
|
||||
serve_coro=server_coroutine,
|
||||
connections=connections,
|
||||
after_start=after_start,
|
||||
before_stop=before_stop,
|
||||
after_stop=after_stop,
|
||||
)
|
||||
|
||||
trigger_events(before_start, loop)
|
||||
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except BaseException:
|
||||
logger.exception("Unable to start server")
|
||||
return
|
||||
|
||||
trigger_events(after_start, loop)
|
||||
|
||||
# Ignore SIGINT when run_multiple
|
||||
if run_multiple:
|
||||
signal_func(SIGINT, SIG_IGN)
|
||||
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals:
|
||||
if OS_IS_WINDOWS:
|
||||
ctrlc_workaround_for_windows(app)
|
||||
else:
|
||||
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
|
||||
loop.add_signal_handler(_signal, app.stop)
|
||||
pid = os.getpid()
|
||||
try:
|
||||
logger.info("Starting worker [%s]", pid)
|
||||
loop.run_forever()
|
||||
finally:
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
|
||||
# Run the on_stop function if provided
|
||||
trigger_events(before_stop, loop)
|
||||
|
||||
# Wait for event loop to finish and all connections to drain
|
||||
http_server.close()
|
||||
loop.run_until_complete(http_server.wait_closed())
|
||||
|
||||
# Complete all tasks on the loop
|
||||
signal.stopped = True
|
||||
for connection in connections:
|
||||
connection.close_if_idle()
|
||||
|
||||
# Gracefully shutdown timeout.
|
||||
# We should provide graceful_shutdown_timeout,
|
||||
# instead of letting connection hangs forever.
|
||||
# Let's roughly calcucate time.
|
||||
graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
||||
start_shutdown: float = 0
|
||||
while connections and (start_shutdown < graceful):
|
||||
loop.run_until_complete(asyncio.sleep(0.1))
|
||||
start_shutdown = start_shutdown + 0.1
|
||||
|
||||
# Force close non-idle connection after waiting for
|
||||
# graceful_shutdown_timeout
|
||||
coros = []
|
||||
for conn in connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(conn.websocket.close_connection())
|
||||
else:
|
||||
conn.close()
|
||||
|
||||
_shutdown = asyncio.gather(*coros)
|
||||
loop.run_until_complete(_shutdown)
|
||||
|
||||
trigger_events(after_stop, loop)
|
||||
|
||||
remove_unix_socket(unix)
|
||||
|
||||
|
||||
def _build_protocol_kwargs(
|
||||
protocol: Type[asyncio.Protocol], config: Config
|
||||
) -> Dict[str, Union[int, float]]:
|
||||
if hasattr(protocol, "websocket_handshake"):
|
||||
return {
|
||||
"websocket_max_size": config.WEBSOCKET_MAX_SIZE,
|
||||
"websocket_max_queue": config.WEBSOCKET_MAX_QUEUE,
|
||||
"websocket_read_limit": config.WEBSOCKET_READ_LIMIT,
|
||||
"websocket_write_limit": config.WEBSOCKET_WRITE_LIMIT,
|
||||
"websocket_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
|
||||
"websocket_ping_interval": config.WEBSOCKET_PING_INTERVAL,
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
|
||||
"""Create TCP server socket.
|
||||
:param host: IPv4, IPv6 or hostname may be specified
|
||||
:param port: TCP port number
|
||||
:param backlog: Maximum number of connections to queue
|
||||
:return: socket.socket object
|
||||
"""
|
||||
try: # IP address: family must be specified for IPv6 at least
|
||||
ip = ip_address(host)
|
||||
host = str(ip)
|
||||
sock = socket.socket(
|
||||
socket.AF_INET6 if ip.version == 6 else socket.AF_INET
|
||||
)
|
||||
except ValueError: # Hostname, may become AF_INET or AF_INET6
|
||||
sock = socket.socket()
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((host, port))
|
||||
sock.listen(backlog)
|
||||
return sock
|
||||
|
||||
|
||||
def bind_unix_socket(path: str, *, mode=0o666, backlog=100) -> socket.socket:
|
||||
"""Create unix socket.
|
||||
:param path: filesystem path
|
||||
:param backlog: Maximum number of connections to queue
|
||||
:return: socket.socket object
|
||||
"""
|
||||
"""Open or atomically replace existing socket with zero downtime."""
|
||||
# Sanitise and pre-verify socket path
|
||||
path = os.path.abspath(path)
|
||||
folder = os.path.dirname(path)
|
||||
if not os.path.isdir(folder):
|
||||
raise FileNotFoundError(f"Socket folder does not exist: {folder}")
|
||||
try:
|
||||
if not stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode):
|
||||
raise FileExistsError(f"Existing file is not a socket: {path}")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
# Create new socket with a random temporary name
|
||||
tmp_path = f"{path}.{secrets.token_urlsafe()}"
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
try:
|
||||
# Critical section begins (filename races)
|
||||
sock.bind(tmp_path)
|
||||
try:
|
||||
os.chmod(tmp_path, mode)
|
||||
# Start listening before rename to avoid connection failures
|
||||
sock.listen(backlog)
|
||||
os.rename(tmp_path, path)
|
||||
except: # noqa: E722
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
finally:
|
||||
raise
|
||||
except: # noqa: E722
|
||||
try:
|
||||
sock.close()
|
||||
finally:
|
||||
raise
|
||||
return sock
|
||||
|
||||
|
||||
def remove_unix_socket(path: Optional[str]) -> None:
|
||||
"""Remove dead unix socket during server exit."""
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
if stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode):
|
||||
# Is it actually dead (doesn't belong to a new server instance)?
|
||||
with socket.socket(socket.AF_UNIX) as testsock:
|
||||
try:
|
||||
testsock.connect(path)
|
||||
except ConnectionRefusedError:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def serve_single(server_settings):
|
||||
main_start = server_settings.pop("main_start", None)
|
||||
main_stop = server_settings.pop("main_stop", None)
|
||||
|
||||
if not server_settings.get("run_async"):
|
||||
# create new event_loop after fork
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
server_settings["loop"] = loop
|
||||
|
||||
trigger_events(main_start, server_settings["loop"])
|
||||
serve(**server_settings)
|
||||
trigger_events(main_stop, server_settings["loop"])
|
||||
|
||||
server_settings["loop"].close()
|
||||
|
||||
|
||||
def serve_multiple(server_settings, workers):
|
||||
"""Start multiple server processes simultaneously. Stop on interrupt
|
||||
and terminate signals, and drain connections when complete.
|
||||
|
||||
:param server_settings: kw arguments to be passed to the serve function
|
||||
:param workers: number of workers to launch
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
:return:
|
||||
"""
|
||||
server_settings["reuse_port"] = True
|
||||
server_settings["run_multiple"] = True
|
||||
|
||||
main_start = server_settings.pop("main_start", None)
|
||||
main_stop = server_settings.pop("main_stop", None)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
trigger_events(main_start, loop)
|
||||
|
||||
# Create a listening socket or use the one in settings
|
||||
sock = server_settings.get("sock")
|
||||
unix = server_settings["unix"]
|
||||
backlog = server_settings["backlog"]
|
||||
if unix:
|
||||
sock = bind_unix_socket(unix, backlog=backlog)
|
||||
server_settings["unix"] = unix
|
||||
if sock is None:
|
||||
sock = bind_socket(
|
||||
server_settings["host"], server_settings["port"], backlog=backlog
|
||||
)
|
||||
sock.set_inheritable(True)
|
||||
server_settings["sock"] = sock
|
||||
server_settings["host"] = None
|
||||
server_settings["port"] = None
|
||||
|
||||
processes = []
|
||||
|
||||
def sig_handler(signal, frame):
|
||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||
for process in processes:
|
||||
os.kill(process.pid, SIGTERM)
|
||||
|
||||
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
||||
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
||||
mp = multiprocessing.get_context("fork")
|
||||
|
||||
for _ in range(workers):
|
||||
process = mp.Process(target=serve, kwargs=server_settings)
|
||||
process.daemon = True
|
||||
process.start()
|
||||
processes.append(process)
|
||||
|
||||
for process in processes:
|
||||
process.join()
|
||||
|
||||
# the above processes will block this until they're stopped
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
|
||||
trigger_events(main_stop, loop)
|
||||
|
||||
sock.close()
|
||||
loop.close()
|
||||
remove_unix_socket(unix)
|
||||
26
sanic/server/__init__.py
Normal file
26
sanic/server/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import asyncio
|
||||
|
||||
from sanic.models.server_types import ConnInfo, Signal
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||
from sanic.server.runners import serve, serve_multiple, serve_single
|
||||
|
||||
|
||||
try:
|
||||
import uvloop # type: ignore
|
||||
|
||||
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AsyncioServer",
|
||||
"ConnInfo",
|
||||
"HttpProtocol",
|
||||
"Signal",
|
||||
"serve",
|
||||
"serve_multiple",
|
||||
"serve_single",
|
||||
)
|
||||
132
sanic/server/async_server.py
Normal file
132
sanic/server/async_server.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from warnings import warn
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
class AsyncioServer:
|
||||
"""
|
||||
Wraps an asyncio server with functionality that might be useful to
|
||||
a user who needs to manage the server lifecycle manually.
|
||||
"""
|
||||
|
||||
__slots__ = ("app", "connections", "loop", "serve_coro", "server")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: Sanic,
|
||||
loop,
|
||||
serve_coro,
|
||||
connections,
|
||||
):
|
||||
# Note, Sanic already called "before_server_start" events
|
||||
# before this helper was even created. So we don't need it here.
|
||||
self.app = app
|
||||
self.connections = connections
|
||||
self.loop = loop
|
||||
self.serve_coro = serve_coro
|
||||
self.server = None
|
||||
|
||||
@property
|
||||
def init(self):
|
||||
warn(
|
||||
"AsyncioServer.init has been deprecated and will be removed "
|
||||
"in v22.6. Use Sanic.state.is_started instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return self.app.state.is_started
|
||||
|
||||
def startup(self):
|
||||
"""
|
||||
Trigger "before_server_start" events
|
||||
"""
|
||||
return self.app._startup()
|
||||
|
||||
def before_start(self):
|
||||
"""
|
||||
Trigger "before_server_start" events
|
||||
"""
|
||||
return self._server_event("init", "before")
|
||||
|
||||
def after_start(self):
|
||||
"""
|
||||
Trigger "after_server_start" events
|
||||
"""
|
||||
return self._server_event("init", "after")
|
||||
|
||||
def before_stop(self):
|
||||
"""
|
||||
Trigger "before_server_stop" events
|
||||
"""
|
||||
return self._server_event("shutdown", "before")
|
||||
|
||||
def after_stop(self):
|
||||
"""
|
||||
Trigger "after_server_stop" events
|
||||
"""
|
||||
return self._server_event("shutdown", "after")
|
||||
|
||||
def is_serving(self) -> bool:
|
||||
if self.server:
|
||||
return self.server.is_serving()
|
||||
return False
|
||||
|
||||
def wait_closed(self):
|
||||
if self.server:
|
||||
return self.server.wait_closed()
|
||||
|
||||
def close(self):
|
||||
if self.server:
|
||||
self.server.close()
|
||||
coro = self.wait_closed()
|
||||
task = asyncio.ensure_future(coro, loop=self.loop)
|
||||
return task
|
||||
|
||||
def start_serving(self):
|
||||
return self._serve(self.server.start_serving)
|
||||
|
||||
def serve_forever(self):
|
||||
return self._serve(self.server.serve_forever)
|
||||
|
||||
def _serve(self, serve_func):
|
||||
if self.server:
|
||||
if not self.app.state.is_started:
|
||||
raise SanicException(
|
||||
"Cannot run Sanic server without first running "
|
||||
"await server.startup()"
|
||||
)
|
||||
|
||||
try:
|
||||
return serve_func()
|
||||
except AttributeError:
|
||||
name = serve_func.__name__
|
||||
raise NotImplementedError(
|
||||
f"server.{name} not available in this version "
|
||||
"of asyncio or uvloop."
|
||||
)
|
||||
|
||||
def _server_event(self, concern: str, action: str):
|
||||
if not self.app.state.is_started:
|
||||
raise SanicException(
|
||||
"Cannot dispatch server event without "
|
||||
"first running await server.startup()"
|
||||
)
|
||||
return self.app._server_event(concern, action, loop=self.loop)
|
||||
|
||||
def __await__(self):
|
||||
"""
|
||||
Starts the asyncio server, returns AsyncServerCoro
|
||||
"""
|
||||
task = asyncio.ensure_future(self.serve_coro)
|
||||
while not task.done():
|
||||
yield
|
||||
self.server = task.result()
|
||||
return self
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user