Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44b657dc59 | ||
|
|
9e889fc20b | ||
|
|
bae2d4cb57 | ||
|
|
492d6fd19d |
@@ -1,24 +0,0 @@
|
|||||||
exclude_patterns:
|
|
||||||
- "sanic/__main__.py"
|
|
||||||
- "sanic/reloader_helpers.py"
|
|
||||||
- "sanic/simple.py"
|
|
||||||
- "sanic/utils.py"
|
|
||||||
- ".github/"
|
|
||||||
- "changelogs/"
|
|
||||||
- "docker/"
|
|
||||||
- "docs/"
|
|
||||||
- "examples/"
|
|
||||||
- "hack/"
|
|
||||||
- "scripts/"
|
|
||||||
- "tests/"
|
|
||||||
checks:
|
|
||||||
argument-count:
|
|
||||||
enabled: false
|
|
||||||
file-lines:
|
|
||||||
config:
|
|
||||||
threshold: 1000
|
|
||||||
method-count:
|
|
||||||
config:
|
|
||||||
threshold: 40
|
|
||||||
complex-logic:
|
|
||||||
enabled: false
|
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
[run]
|
[run]
|
||||||
branch = True
|
branch = True
|
||||||
source = sanic
|
source = sanic
|
||||||
omit =
|
omit = site-packages, sanic/utils.py, sanic/__main__.py
|
||||||
site-packages
|
|
||||||
sanic/__main__.py
|
|
||||||
sanic/reloader_helpers.py
|
|
||||||
sanic/simple.py
|
|
||||||
sanic/utils.py
|
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
directory = coverage
|
directory = coverage
|
||||||
|
|||||||
37
.github/workflows/codeql-analysis.yml
vendored
37
.github/workflows/codeql-analysis.yml
vendored
@@ -1,10 +1,22 @@
|
|||||||
|
# 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"
|
name: "CodeQL"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '25 16 * * 0'
|
- cron: '25 16 * * 0'
|
||||||
|
|
||||||
@@ -17,18 +29,39 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'python' ]
|
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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
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
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
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
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v1
|
||||||
|
|||||||
40
.github/workflows/coverage.yml
vendored
40
.github/workflows/coverage.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Coverage check
|
|
||||||
# on:
|
|
||||||
# push:
|
|
||||||
# branches:
|
|
||||||
# - main
|
|
||||||
# tags:
|
|
||||||
# - "!*" # Do not execute on tags
|
|
||||||
# paths:
|
|
||||||
# - sanic/*
|
|
||||||
# - tests/*
|
|
||||||
# pull_request:
|
|
||||||
# paths:
|
|
||||||
# - "!*.MD"
|
|
||||||
on: [push, pull_request]
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
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
39
.github/workflows/on-demand.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
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"
|
|
||||||
32
.github/workflows/pr-bandit.yml
vendored
32
.github/workflows/pr-bandit.yml
vendored
@@ -1,32 +0,0 @@
|
|||||||
name: Security Analysis
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bandit:
|
|
||||||
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}
|
|
||||||
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 }}"
|
|
||||||
29
.github/workflows/pr-docs.yml
vendored
29
.github/workflows/pr-docs.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: Document Linter
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docsLinter:
|
|
||||||
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 }}"
|
|
||||||
30
.github/workflows/pr-linter.yml
vendored
30
.github/workflows/pr-linter.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Linter Checks
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
linter:
|
|
||||||
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
41
.github/workflows/pr-python-pypy.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
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"
|
|
||||||
38
.github/workflows/pr-python37.yml
vendored
38
.github/workflows/pr-python37.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Python 3.7 Tests
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- sanic/*
|
|
||||||
- tests/*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testPy37:
|
|
||||||
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.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"
|
|
||||||
38
.github/workflows/pr-python38.yml
vendored
38
.github/workflows/pr-python38.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Python 3.8 Tests
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- sanic/*
|
|
||||||
- tests/*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testPy38:
|
|
||||||
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.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"
|
|
||||||
50
.github/workflows/pr-python39.yml
vendored
50
.github/workflows/pr-python39.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
name: Python 3.9 Tests
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- sanic/*
|
|
||||||
- tests/*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testPy39:
|
|
||||||
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.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"
|
|
||||||
32
.github/workflows/pr-type-check.yml
vendored
32
.github/workflows/pr-type-check.yml
vendored
@@ -1,32 +0,0 @@
|
|||||||
name: Typing Checks
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
typeChecking:
|
|
||||||
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}
|
|
||||||
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 }}"
|
|
||||||
34
.github/workflows/pr-windows.yml
vendored
34
.github/workflows/pr-windows.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Run Unit Tests on Windows
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testsOnWindows:
|
|
||||||
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: 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
48
.github/workflows/publish-images.yml
vendored
@@ -1,48 +0,0 @@
|
|||||||
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"]
|
|
||||||
|
|
||||||
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
28
.github/workflows/publish-package.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
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,7 +6,6 @@
|
|||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
coverage
|
coverage
|
||||||
coverage.xml
|
|
||||||
.tox
|
.tox
|
||||||
settings.py
|
settings.py
|
||||||
.idea/*
|
.idea/*
|
||||||
@@ -19,6 +18,3 @@ build/*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist/*
|
dist/*
|
||||||
pip-wheel-metadata/
|
pip-wheel-metadata/
|
||||||
.pytest_cache/*
|
|
||||||
.venv/*
|
|
||||||
.vscode/*
|
|
||||||
|
|||||||
94
.travis.yml
Normal file
94
.travis.yml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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"
|
||||||
111
CHANGELOG.rst
111
CHANGELOG.rst
@@ -1,114 +1,3 @@
|
|||||||
.. 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
|
Version 21.3.2
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ a virtual environment already set up, then run:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install -e ".[dev]"
|
pip3 install -e . ".[dev]"
|
||||||
|
|
||||||
Dependency Changes
|
Dependency Changes
|
||||||
------------------
|
------------------
|
||||||
@@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks.
|
|||||||
tox -e lint
|
tox -e lint
|
||||||
|
|
||||||
Run type annotation checks
|
Run type annotation checks
|
||||||
--------------------------
|
---------------
|
||||||
|
|
||||||
``tox`` environment -> ``[testenv:type-checking]``
|
``tox`` environment -> ``[testenv:type-checking]``
|
||||||
|
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -49,9 +49,6 @@ test: clean
|
|||||||
test-coverage: clean
|
test-coverage: clean
|
||||||
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
|
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
|
||||||
|
|
||||||
view-coverage:
|
|
||||||
sanic ./coverage --simple
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
python setup.py install
|
python setup.py install
|
||||||
|
|
||||||
@@ -88,7 +85,8 @@ docs-test: docs-clean
|
|||||||
cd docs && make dummy
|
cd docs && make dummy
|
||||||
|
|
||||||
docs-serve:
|
docs-serve:
|
||||||
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./
|
# python -m http.server --directory=./docs/_build/html 9999
|
||||||
|
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
python scripts/changelog.py
|
python scripts/changelog.py
|
||||||
|
|||||||
24
README.rst
24
README.rst
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
|||||||
:stub-columns: 1
|
:stub-columns: 1
|
||||||
|
|
||||||
* - Build
|
* - Build
|
||||||
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
|
- | |Build Status| |AppVeyor Build Status| |Codecov|
|
||||||
* - Docs
|
* - Docs
|
||||||
- | |UserGuide| |Documentation|
|
- | |UserGuide| |Documentation|
|
||||||
* - Package
|
* - Package
|
||||||
@@ -29,12 +29,10 @@ Sanic | Build fast. Run fast.
|
|||||||
:target: https://discord.gg/FARQzAEMAA
|
:target: https://discord.gg/FARQzAEMAA
|
||||||
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/sanic-org/sanic
|
:target: https://codecov.io/gh/sanic-org/sanic
|
||||||
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
|
.. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master
|
||||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
|
:target: https://travis-ci.com/sanic-org/sanic
|
||||||
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
|
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml
|
:target: https://ci.appveyor.com/project/sanic-org/sanic
|
||||||
.. |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
|
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||||
@@ -77,7 +75,17 @@ The goal of the project is to provide a simple way to get up and running a highl
|
|||||||
Sponsor
|
Sponsor
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
|Try CodeStream|
|
||||||
|
|
||||||
|
.. |Try CodeStream| image:: https://alt-images.codestream.com/codestream_logo_sanicorg.png
|
||||||
|
:target: https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner
|
||||||
|
:alt: Try CodeStream
|
||||||
|
|
||||||
|
Manage pull requests and conduct code reviews in your IDE with full source-tree context. Comment on any line, not just the diffs. Use jump-to-definition, your favorite keybindings, and code intelligence with more of your workflow.
|
||||||
|
|
||||||
|
`Learn More <https://codestream.com/?utm_source=github&utm_campaign=sanicorg&utm_medium=banner>`_
|
||||||
|
|
||||||
|
Thank you to our sponsor. Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|||||||
14
codecov.yml
Normal file
14
codecov.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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,9 +1,28 @@
|
|||||||
ARG BASE_IMAGE_TAG
|
FROM alpine:3.7
|
||||||
|
|
||||||
FROM sanicframework/sanic-build:${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
|
||||||
|
|
||||||
RUN apk update
|
|
||||||
RUN update-ca-certificates
|
RUN update-ca-certificates
|
||||||
|
RUN rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
RUN pip install sanic
|
ENV PYENV_ROOT="/root/.pyenv"
|
||||||
RUN apk del build-base
|
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"]
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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/*
|
|
||||||
11
docker/bin/entrypoint.sh
Executable file
11
docker/bin/entrypoint.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
eval "$(pyenv init -)"
|
||||||
|
eval "$(pyenv virtualenv-init -)"
|
||||||
|
source /root/.pyenv/completions/pyenv.bash
|
||||||
|
|
||||||
|
pip install tox
|
||||||
|
|
||||||
|
exec $@
|
||||||
|
|
||||||
17
docker/bin/install_python.sh
Executable file
17
docker/bin/install_python.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/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,8 +10,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
# Add support for auto-doc
|
# Add support for auto-doc
|
||||||
|
import recommonmark
|
||||||
|
|
||||||
|
from recommonmark.transform import AutoStructify
|
||||||
|
|
||||||
|
|
||||||
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
||||||
@@ -24,7 +26,7 @@ import sanic
|
|||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
extensions = ["sphinx.ext.autodoc", "m2r2"]
|
extensions = ["sphinx.ext.autodoc", "recommonmark"]
|
||||||
|
|
||||||
templates_path = ["_templates"]
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
@@ -160,6 +162,20 @@ autodoc_default_options = {
|
|||||||
"member-order": "groupwise",
|
"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 = {
|
html_theme_options = {
|
||||||
"style_external_links": False,
|
"style_external_links": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
Application
|
|
||||||
===========
|
|
||||||
|
|
||||||
sanic.app
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. automodule:: sanic.app
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:inherited-members:
|
|
||||||
|
|
||||||
sanic.config
|
|
||||||
------------
|
|
||||||
|
|
||||||
.. automodule:: sanic.config
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
Blueprints
|
|
||||||
==========
|
|
||||||
|
|
||||||
sanic.blueprints
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. automodule:: sanic.blueprints
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:inherited-members:
|
|
||||||
|
|
||||||
sanic.blueprint_group
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
.. automodule:: sanic.blueprint_group
|
|
||||||
:members:
|
|
||||||
:special-members:
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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:
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
Exceptions
|
|
||||||
==========
|
|
||||||
|
|
||||||
sanic.errorpages
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. automodule:: sanic.errorpages
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
sanic.exceptions
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. automodule:: sanic.exceptions
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
Routing
|
|
||||||
=======
|
|
||||||
|
|
||||||
sanic_routing models
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. autoclass:: sanic_routing.route::Route
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: sanic_routing.group::RouteGroup
|
|
||||||
:members:
|
|
||||||
|
|
||||||
sanic.router
|
|
||||||
------------
|
|
||||||
|
|
||||||
.. automodule:: sanic.router
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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:
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
Utility
|
|
||||||
=======
|
|
||||||
|
|
||||||
sanic.compat
|
|
||||||
------------
|
|
||||||
|
|
||||||
.. automodule:: sanic.compat
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
sanic.log
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. automodule:: sanic.log
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,13 +1,132 @@
|
|||||||
📑 API Reference
|
📑 API Reference
|
||||||
================
|
================
|
||||||
|
|
||||||
.. toctree::
|
sanic.app
|
||||||
:maxdepth: 2
|
---------
|
||||||
|
|
||||||
api/app
|
.. automodule:: sanic.app
|
||||||
api/blueprints
|
:members:
|
||||||
api/core
|
:show-inheritance:
|
||||||
api/exceptions
|
:inherited-members:
|
||||||
api/router
|
|
||||||
api/server
|
sanic.blueprints
|
||||||
api/utility
|
----------------
|
||||||
|
|
||||||
|
.. 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:
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
📜 Changelog
|
📜 Changelog
|
||||||
============
|
============
|
||||||
|
|
||||||
.. mdinclude:: ./releases/21.9.md
|
|
||||||
|
|
||||||
.. include:: ../../CHANGELOG.rst
|
.. include:: ../../CHANGELOG.rst
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
## 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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,44 +1,29 @@
|
|||||||
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
from signal import signal, SIGINT
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from signal import SIGINT, signal
|
|
||||||
|
|
||||||
import uvloop
|
import uvloop
|
||||||
|
|
||||||
from sanic import Sanic, response
|
|
||||||
from sanic.server import AsyncioServer
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
@app.listener('after_server_start')
|
||||||
@app.listener("after_server_start")
|
|
||||||
async def after_start_test(app, loop):
|
async def after_start_test(app, loop):
|
||||||
print("Async Server Started!")
|
print("Async Server Started!")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return response.json({"answer": "42"})
|
return response.json({"answer": "42"})
|
||||||
|
|
||||||
|
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
serv_coro = app.create_server(
|
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
host="0.0.0.0", port=8000, return_asyncio_server=True
|
|
||||||
)
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
||||||
signal(SIGINT, lambda s, f: loop.stop())
|
signal(SIGINT, lambda s, f: loop.stop())
|
||||||
server: AsyncioServer = loop.run_until_complete(serv_task) # type: ignore
|
server = loop.run_until_complete(serv_task)
|
||||||
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.
|
|
||||||
server.before_start()
|
|
||||||
server.after_start()
|
server.after_start()
|
||||||
try:
|
try:
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt as e:
|
||||||
loop.stop()
|
loop.stop()
|
||||||
finally:
|
finally:
|
||||||
server.before_stop()
|
server.before_stop()
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import redirect
|
from sanic.response import file
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
app.static('index.html', "websocket.html")
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index(request):
|
async def index(request):
|
||||||
return redirect("index.html")
|
return await file('websocket.html')
|
||||||
|
|
||||||
|
|
||||||
@app.websocket('/feed')
|
@app.websocket('/feed')
|
||||||
async def feed(request, ws):
|
async def feed(request, ws):
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
FROM catthehacker/ubuntu:act-latest
|
|
||||||
SHELL [ "/bin/bash", "-c" ]
|
|
||||||
ENTRYPOINT []
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install gcc -y
|
|
||||||
RUN apt-get install -y --no-install-recommends g++
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["setuptools", "wheel"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from sanic.__version__ import __version__
|
from sanic.__version__ import __version__
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.constants import HTTPMethod
|
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import HTTPResponse, html, json, text
|
from sanic.response import HTTPResponse, html, json, text
|
||||||
|
|
||||||
@@ -10,7 +9,6 @@ __all__ = (
|
|||||||
"__version__",
|
"__version__",
|
||||||
"Sanic",
|
"Sanic",
|
||||||
"Blueprint",
|
"Blueprint",
|
||||||
"HTTPMethod",
|
|
||||||
"HTTPResponse",
|
"HTTPResponse",
|
||||||
"Request",
|
"Request",
|
||||||
"html",
|
"html",
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from sanic_routing import __version__ as __routing_version__ # type: ignore
|
|
||||||
|
|
||||||
from sanic import __version__
|
from sanic import __version__
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.config import BASE_LOGO
|
from sanic.config import BASE_LOGO
|
||||||
from sanic.log import error_logger
|
from sanic.log import logger
|
||||||
from sanic.simple import create_simple_server
|
|
||||||
|
|
||||||
|
|
||||||
class SanicArgumentParser(ArgumentParser):
|
class SanicArgumentParser(ArgumentParser):
|
||||||
def add_bool_arguments(self, *args, **kwargs):
|
def add_bool_arguments(self, *args, **kwargs):
|
||||||
group = self.add_mutually_exclusive_group()
|
group = self.add_mutually_exclusive_group()
|
||||||
group.add_argument(*args, action="store_true", **kwargs)
|
group.add_argument(*args, action="store_true", **kwargs)
|
||||||
kwargs["help"] = f"no {kwargs['help']}\n "
|
kwargs["help"] = "no " + kwargs["help"]
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||||
)
|
)
|
||||||
@@ -29,30 +25,7 @@ def main():
|
|||||||
parser = SanicArgumentParser(
|
parser = SanicArgumentParser(
|
||||||
prog="sanic",
|
prog="sanic",
|
||||||
description=BASE_LOGO,
|
description=BASE_LOGO,
|
||||||
formatter_class=lambda prog: RawTextHelpFormatter(
|
formatter_class=RawDescriptionHelpFormatter,
|
||||||
prog, max_help_position=33
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-v",
|
|
||||||
"--version",
|
|
||||||
action="version",
|
|
||||||
version=f"Sanic {__version__}; Routing {__routing_version__}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--factory",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Treat app as an application factory, "
|
|
||||||
"i.e. a () -> <Sanic app> callable"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-s",
|
|
||||||
"--simple",
|
|
||||||
dest="simple",
|
|
||||||
action="store_true",
|
|
||||||
help="Run Sanic as a Simple Server (module arg should be a path)\n ",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-H",
|
"-H",
|
||||||
@@ -60,7 +33,7 @@ def main():
|
|||||||
dest="host",
|
dest="host",
|
||||||
type=str,
|
type=str,
|
||||||
default="127.0.0.1",
|
default="127.0.0.1",
|
||||||
help="Host address [default 127.0.0.1]",
|
help="host address [default 127.0.0.1]",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p",
|
"-p",
|
||||||
@@ -68,7 +41,7 @@ def main():
|
|||||||
dest="port",
|
dest="port",
|
||||||
type=int,
|
type=int,
|
||||||
default=8000,
|
default=8000,
|
||||||
help="Port to serve on [default 8000]",
|
help="port to serve on [default 8000]",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-u",
|
"-u",
|
||||||
@@ -76,16 +49,13 @@ def main():
|
|||||||
dest="unix",
|
dest="unix",
|
||||||
type=str,
|
type=str,
|
||||||
default="",
|
default="",
|
||||||
help="location of unix socket\n ",
|
help="location of unix socket",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--cert", dest="cert", type=str, help="Location of certificate for SSL"
|
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
|
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
||||||
)
|
|
||||||
parser.add_bool_arguments(
|
|
||||||
"--access-logs", dest="access_log", help="display access logs"
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-w",
|
"-w",
|
||||||
@@ -93,31 +63,20 @@ def main():
|
|||||||
dest="workers",
|
dest="workers",
|
||||||
type=int,
|
type=int,
|
||||||
default=1,
|
default=1,
|
||||||
help="number of worker processes [default 1]\n ",
|
help="number of worker processes [default 1]",
|
||||||
)
|
)
|
||||||
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
|
parser.add_argument("--debug", dest="debug", action="store_true")
|
||||||
parser.add_argument(
|
parser.add_bool_arguments(
|
||||||
"-r",
|
"--access-logs", dest="access_log", help="display access logs"
|
||||||
"--reload",
|
|
||||||
"--auto-reload",
|
|
||||||
dest="auto_reload",
|
|
||||||
action="store_true",
|
|
||||||
help="Watch source directory for file changes and reload on changes",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-R",
|
"-v",
|
||||||
"--reload-dir",
|
"--version",
|
||||||
dest="path",
|
action="version",
|
||||||
action="append",
|
version=f"Sanic {__version__}",
|
||||||
help="Extra directories to watch and reload on changes\n ",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"module",
|
"module", help="path to your Sanic app. Example: path.to.server:app"
|
||||||
help=(
|
|
||||||
"Path to your Sanic app. Example: path.to.server:app\n"
|
|
||||||
"If running a Simple Server, path to directory to serve. "
|
|
||||||
"Example: ./\n"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -126,71 +85,47 @@ def main():
|
|||||||
if module_path not in sys.path:
|
if module_path not in sys.path:
|
||||||
sys.path.append(module_path)
|
sys.path.append(module_path)
|
||||||
|
|
||||||
if args.simple:
|
if ":" in args.module:
|
||||||
path = Path(args.module)
|
module_name, app_name = args.module.rsplit(":", 1)
|
||||||
app = create_simple_server(path)
|
|
||||||
else:
|
else:
|
||||||
delimiter = ":" if ":" in args.module else "."
|
module_parts = args.module.split(".")
|
||||||
module_name, app_name = args.module.rsplit(delimiter, 1)
|
module_name = ".".join(module_parts[:-1])
|
||||||
|
app_name = module_parts[-1]
|
||||||
|
|
||||||
if app_name.endswith("()"):
|
module = import_module(module_name)
|
||||||
args.factory = True
|
app = getattr(module, app_name, None)
|
||||||
app_name = app_name[:-2]
|
app_name = type(app).__name__
|
||||||
|
|
||||||
module = import_module(module_name)
|
if not isinstance(app, Sanic):
|
||||||
app = getattr(module, app_name, None)
|
raise ValueError(
|
||||||
if args.factory:
|
f"Module is not a Sanic app, it is a {app_name}. "
|
||||||
app = app()
|
f"Perhaps you meant {args.module}.app?"
|
||||||
|
)
|
||||||
app_type_name = type(app).__name__
|
|
||||||
|
|
||||||
if not isinstance(app, Sanic):
|
|
||||||
raise ValueError(
|
|
||||||
f"Module is not a Sanic app, it is a {app_type_name}. "
|
|
||||||
f"Perhaps you meant {args.module}.app?"
|
|
||||||
)
|
|
||||||
if args.cert is not None or args.key is not None:
|
if args.cert is not None or args.key is not None:
|
||||||
ssl: Optional[Dict[str, Any]] = {
|
ssl = {
|
||||||
"cert": args.cert,
|
"cert": args.cert,
|
||||||
"key": args.key,
|
"key": args.key,
|
||||||
}
|
} # type: Optional[Dict[str, Any]]
|
||||||
else:
|
else:
|
||||||
ssl = None
|
ssl = None
|
||||||
|
|
||||||
kwargs = {
|
app.run(
|
||||||
"host": args.host,
|
host=args.host,
|
||||||
"port": args.port,
|
port=args.port,
|
||||||
"unix": args.unix,
|
unix=args.unix,
|
||||||
"workers": args.workers,
|
workers=args.workers,
|
||||||
"debug": args.debug,
|
debug=args.debug,
|
||||||
"access_log": args.access_log,
|
access_log=args.access_log,
|
||||||
"ssl": ssl,
|
ssl=ssl,
|
||||||
}
|
)
|
||||||
if args.auto_reload:
|
|
||||||
kwargs["auto_reload"] = True
|
|
||||||
|
|
||||||
if args.path:
|
|
||||||
if args.auto_reload or args.debug:
|
|
||||||
kwargs["reload_dir"] = 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."
|
|
||||||
)
|
|
||||||
|
|
||||||
app.run(**kwargs)
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
if module_name.startswith(e.name):
|
logger.error(
|
||||||
error_logger.error(
|
f"No module named {e.name} found.\n"
|
||||||
f"No module named {e.name} found.\n"
|
f" Example File: project/sanic_server.py -> app\n"
|
||||||
" Example File: project/sanic_server.py -> app\n"
|
f" Example Module: project.sanic_server.app"
|
||||||
" Example Module: project.sanic_server.app"
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
error_logger.exception("Failed to run app")
|
logger.exception("Failed to run app")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "21.9.3"
|
__version__ = "21.3.4"
|
||||||
|
|||||||
446
sanic/app.py
446
sanic/app.py
@@ -1,12 +1,9 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from asyncio import (
|
from asyncio import (
|
||||||
AbstractEventLoop,
|
|
||||||
CancelledError,
|
CancelledError,
|
||||||
Protocol,
|
Protocol,
|
||||||
ensure_future,
|
ensure_future,
|
||||||
@@ -17,14 +14,12 @@ from asyncio.futures import Future
|
|||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from pathlib import Path
|
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from ssl import Purpose, SSLContext, create_default_context
|
from ssl import Purpose, SSLContext, create_default_context
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
AnyStr,
|
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
Coroutine,
|
||||||
@@ -34,7 +29,6 @@ from typing import (
|
|||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
@@ -49,7 +43,7 @@ from sanic.asgi import ASGIApp
|
|||||||
from sanic.base import BaseSanic
|
from sanic.base import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.config import BASE_LOGO, SANIC_PREFIX, Config
|
from sanic.config import BASE_LOGO, Config
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
SanicException,
|
SanicException,
|
||||||
@@ -74,29 +68,19 @@ from sanic.router import Router
|
|||||||
from sanic.server import AsyncioServer, HttpProtocol
|
from sanic.server import AsyncioServer, HttpProtocol
|
||||||
from sanic.server import Signal as ServerSignal
|
from sanic.server import Signal as ServerSignal
|
||||||
from sanic.server import serve, serve_multiple, serve_single
|
from sanic.server import serve, serve_multiple, serve_single
|
||||||
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
|
||||||
from sanic.server.websockets.impl import ConnectionClosed
|
|
||||||
from sanic.signals import Signal, SignalRouter
|
from sanic.signals import Signal, SignalRouter
|
||||||
from sanic.touchup import TouchUp, TouchUpMeta
|
from sanic.websocket import ConnectionClosed, WebSocketProtocol
|
||||||
|
|
||||||
|
|
||||||
class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
class Sanic(BaseSanic):
|
||||||
"""
|
"""
|
||||||
The main application instance
|
The main application instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__touchup__ = (
|
|
||||||
"handle_request",
|
|
||||||
"handle_exception",
|
|
||||||
"_run_response_middleware",
|
|
||||||
"_run_request_middleware",
|
|
||||||
)
|
|
||||||
__fake_slots__ = (
|
__fake_slots__ = (
|
||||||
"_asgi_app",
|
|
||||||
"_app_registry",
|
"_app_registry",
|
||||||
"_asgi_client",
|
"_asgi_client",
|
||||||
"_blueprint_order",
|
"_blueprint_order",
|
||||||
"_delayed_tasks",
|
|
||||||
"_future_routes",
|
"_future_routes",
|
||||||
"_future_statics",
|
"_future_statics",
|
||||||
"_future_middleware",
|
"_future_middleware",
|
||||||
@@ -105,7 +89,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"_future_signals",
|
"_future_signals",
|
||||||
"_test_client",
|
"_test_client",
|
||||||
"_test_manager",
|
"_test_manager",
|
||||||
"auto_reload",
|
|
||||||
"asgi",
|
"asgi",
|
||||||
"blueprints",
|
"blueprints",
|
||||||
"config",
|
"config",
|
||||||
@@ -120,7 +103,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"name",
|
"name",
|
||||||
"named_request_middleware",
|
"named_request_middleware",
|
||||||
"named_response_middleware",
|
"named_response_middleware",
|
||||||
"reload_dirs",
|
|
||||||
"request_class",
|
"request_class",
|
||||||
"request_middleware",
|
"request_middleware",
|
||||||
"response_middleware",
|
"response_middleware",
|
||||||
@@ -139,55 +121,45 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
config: Optional[Config] = None,
|
|
||||||
ctx: Optional[Any] = None,
|
|
||||||
router: Optional[Router] = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: Optional[SignalRouter] = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: Optional[ErrorHandler] = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
load_env: Union[bool, str] = True,
|
load_env: bool = True,
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
|
||||||
request_class: Optional[Type[Request]] = None,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
log_config: Optional[Dict[str, Any]] = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
register: Optional[bool] = None,
|
register: Optional[bool] = None,
|
||||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
dumps: Optional[Callable[..., str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name=name)
|
super().__init__()
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
raise SanicException(
|
||||||
|
"Sanic instance cannot be unnamed. "
|
||||||
|
"Please use Sanic(name='your_application_name') instead.",
|
||||||
|
)
|
||||||
# logging
|
# logging
|
||||||
if configure_logging:
|
if configure_logging:
|
||||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||||
|
|
||||||
if config and (load_env is not True or env_prefix != SANIC_PREFIX):
|
|
||||||
raise SanicException(
|
|
||||||
"When instantiating Sanic with config, you cannot also pass "
|
|
||||||
"load_env or env_prefix"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._asgi_client = None
|
self._asgi_client = None
|
||||||
self._blueprint_order: List[Blueprint] = []
|
self._blueprint_order: List[Blueprint] = []
|
||||||
self._delayed_tasks: List[str] = []
|
|
||||||
self._test_client = None
|
self._test_client = None
|
||||||
self._test_manager = None
|
self._test_manager = None
|
||||||
self.asgi = False
|
self.asgi = False
|
||||||
self.auto_reload = False
|
|
||||||
self.blueprints: Dict[str, Blueprint] = {}
|
self.blueprints: Dict[str, Blueprint] = {}
|
||||||
self.config: Config = config or Config(
|
self.config = Config(load_env=load_env)
|
||||||
load_env=load_env,
|
self.configure_logging = configure_logging
|
||||||
env_prefix=env_prefix,
|
self.ctx = SimpleNamespace()
|
||||||
app=self,
|
|
||||||
)
|
|
||||||
self.configure_logging: bool = configure_logging
|
|
||||||
self.ctx: Any = ctx or SimpleNamespace()
|
|
||||||
self.debug = None
|
self.debug = None
|
||||||
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
self.error_handler = error_handler or ErrorHandler()
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.is_stopping = False
|
self.is_stopping = False
|
||||||
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
|
||||||
|
self.name = name
|
||||||
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
self.reload_dirs: Set[Path] = set()
|
|
||||||
self.request_class = request_class
|
self.request_class = request_class
|
||||||
self.request_middleware: Deque[MiddlewareType] = deque()
|
self.request_middleware: Deque[MiddlewareType] = deque()
|
||||||
self.response_middleware: Deque[MiddlewareType] = deque()
|
self.response_middleware: Deque[MiddlewareType] = deque()
|
||||||
@@ -203,14 +175,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
if register is not None:
|
if register is not None:
|
||||||
self.config.REGISTER = register
|
self.config.REGISTER = register
|
||||||
|
|
||||||
if self.config.REGISTER:
|
if self.config.REGISTER:
|
||||||
self.__class__.register_app(self)
|
self.__class__.register_app(self)
|
||||||
|
|
||||||
self.router.ctx.app = self
|
self.router.ctx.app = self
|
||||||
self.signal_router.ctx.app = self
|
|
||||||
|
|
||||||
if dumps:
|
if dumps:
|
||||||
BaseHTTPResponse._dumps = dumps # type: ignore
|
BaseHTTPResponse._dumps = dumps
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loop(self):
|
def loop(self):
|
||||||
@@ -248,12 +220,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
loop = self.loop # Will raise SanicError if loop is not started
|
loop = self.loop # Will raise SanicError if loop is not started
|
||||||
self._loop_add_task(task, self, loop)
|
self._loop_add_task(task, self, loop)
|
||||||
except SanicException:
|
except SanicException:
|
||||||
task_name = f"sanic.delayed_task.{hash(task)}"
|
self.listener("before_server_start")(
|
||||||
if not self._delayed_tasks:
|
partial(self._loop_add_task, task)
|
||||||
self.after_server_start(partial(self.dispatch_delayed_tasks))
|
)
|
||||||
|
|
||||||
self.signal(task_name)(partial(self.run_delayed_task, task=task))
|
|
||||||
self._delayed_tasks.append(task_name)
|
|
||||||
|
|
||||||
def register_listener(self, listener: Callable, event: str) -> Any:
|
def register_listener(self, listener: Callable, event: str) -> Any:
|
||||||
"""
|
"""
|
||||||
@@ -265,20 +234,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_event = ListenerEvent[event.upper()]
|
_event = ListenerEvent(event)
|
||||||
except (ValueError, AttributeError):
|
except ValueError:
|
||||||
valid = ", ".join(
|
valid = ", ".join(ListenerEvent.__members__.values())
|
||||||
map(lambda x: x.lower(), ListenerEvent.__members__.keys())
|
|
||||||
)
|
|
||||||
raise InvalidUsage(f"Invalid event: {event}. Use one of: {valid}")
|
raise InvalidUsage(f"Invalid event: {event}. Use one of: {valid}")
|
||||||
|
|
||||||
if "." in _event:
|
self.listeners[_event].append(listener)
|
||||||
self.signal(_event.value)(
|
|
||||||
partial(self._listener, listener=listener)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.listeners[_event.value].append(listener)
|
|
||||||
|
|
||||||
return listener
|
return listener
|
||||||
|
|
||||||
def register_middleware(self, middleware, attach_to: str = "request"):
|
def register_middleware(self, middleware, attach_to: str = "request"):
|
||||||
@@ -337,11 +298,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
self.named_response_middleware[_rn].appendleft(middleware)
|
self.named_response_middleware[_rn].appendleft(middleware)
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
def _apply_exception_handler(
|
def _apply_exception_handler(self, handler: FutureException):
|
||||||
self,
|
|
||||||
handler: FutureException,
|
|
||||||
route_names: Optional[List[str]] = None,
|
|
||||||
):
|
|
||||||
"""Decorate a function to be registered as a handler for exceptions
|
"""Decorate a function to be registered as a handler for exceptions
|
||||||
|
|
||||||
:param exceptions: exceptions
|
:param exceptions: exceptions
|
||||||
@@ -351,9 +308,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
for exception in handler.exceptions:
|
for exception in handler.exceptions:
|
||||||
if isinstance(exception, (tuple, list)):
|
if isinstance(exception, (tuple, list)):
|
||||||
for e in exception:
|
for e in exception:
|
||||||
self.error_handler.add(e, handler.handler, route_names)
|
self.error_handler.add(e, handler.handler)
|
||||||
else:
|
else:
|
||||||
self.error_handler.add(exception, handler.handler, route_names)
|
self.error_handler.add(exception, handler.handler)
|
||||||
return handler.handler
|
return handler.handler
|
||||||
|
|
||||||
def _apply_listener(self, listener: FutureListener):
|
def _apply_listener(self, listener: FutureListener):
|
||||||
@@ -410,32 +367,18 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
*,
|
*,
|
||||||
condition: Optional[Dict[str, str]] = None,
|
condition: Optional[Dict[str, str]] = None,
|
||||||
context: Optional[Dict[str, Any]] = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
fail_not_found: bool = True,
|
|
||||||
inline: bool = False,
|
|
||||||
reverse: bool = False,
|
|
||||||
) -> Coroutine[Any, Any, Awaitable[Any]]:
|
) -> Coroutine[Any, Any, Awaitable[Any]]:
|
||||||
return self.signal_router.dispatch(
|
return self.signal_router.dispatch(
|
||||||
event,
|
event,
|
||||||
context=context,
|
context=context,
|
||||||
condition=condition,
|
condition=condition,
|
||||||
inline=inline,
|
|
||||||
reverse=reverse,
|
|
||||||
fail_not_found=fail_not_found,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def event(
|
def event(self, event: str, timeout: Optional[Union[int, float]] = None):
|
||||||
self, event: str, timeout: Optional[Union[int, float]] = None
|
|
||||||
):
|
|
||||||
signal = self.signal_router.name_index.get(event)
|
signal = self.signal_router.name_index.get(event)
|
||||||
if not signal:
|
if not signal:
|
||||||
if self.config.EVENT_AUTOREGISTER:
|
raise NotFound("Could not find signal %s" % event)
|
||||||
self.signal_router.reset()
|
return wait_for(signal.ctx.event.wait(), timeout=timeout)
|
||||||
self.add_signal(None, event)
|
|
||||||
signal = self.signal_router.name_index[event]
|
|
||||||
self.signal_router.finalize()
|
|
||||||
else:
|
|
||||||
raise NotFound("Could not find signal %s" % event)
|
|
||||||
return await wait_for(signal.ctx.event.wait(), timeout=timeout)
|
|
||||||
|
|
||||||
def enable_websocket(self, enable=True):
|
def enable_websocket(self, enable=True):
|
||||||
"""Enable or disable the support for websocket.
|
"""Enable or disable the support for websocket.
|
||||||
@@ -450,13 +393,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
self.websocket_enabled = enable
|
self.websocket_enabled = enable
|
||||||
|
|
||||||
def blueprint(
|
def blueprint(self, blueprint, **options):
|
||||||
self,
|
|
||||||
blueprint: Union[
|
|
||||||
Blueprint, List[Blueprint], Tuple[Blueprint], BlueprintGroup
|
|
||||||
],
|
|
||||||
**options: Any,
|
|
||||||
):
|
|
||||||
"""Register a blueprint on the application.
|
"""Register a blueprint on the application.
|
||||||
|
|
||||||
:param blueprint: Blueprint object or (list, tuple) thereof
|
:param blueprint: Blueprint object or (list, tuple) thereof
|
||||||
@@ -465,33 +402,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
||||||
for item in blueprint:
|
for item in blueprint:
|
||||||
params = {**options}
|
self.blueprint(item, **options)
|
||||||
if isinstance(blueprint, BlueprintGroup):
|
|
||||||
if blueprint.url_prefix:
|
|
||||||
merge_from = [
|
|
||||||
options.get("url_prefix", ""),
|
|
||||||
blueprint.url_prefix,
|
|
||||||
]
|
|
||||||
if not isinstance(item, BlueprintGroup):
|
|
||||||
merge_from.append(item.url_prefix or "")
|
|
||||||
merged_prefix = "/".join(
|
|
||||||
u.strip("/") for u in merge_from
|
|
||||||
).rstrip("/")
|
|
||||||
params["url_prefix"] = f"/{merged_prefix}"
|
|
||||||
|
|
||||||
for _attr in ["version", "strict_slashes"]:
|
|
||||||
if getattr(item, _attr) is None:
|
|
||||||
params[_attr] = getattr(
|
|
||||||
blueprint, _attr
|
|
||||||
) or options.get(_attr)
|
|
||||||
if item.version_prefix == "/v":
|
|
||||||
if blueprint.version_prefix == "/v":
|
|
||||||
params["version_prefix"] = options.get(
|
|
||||||
"version_prefix"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
params["version_prefix"] = blueprint.version_prefix
|
|
||||||
self.blueprint(item, **params)
|
|
||||||
return
|
return
|
||||||
if blueprint.name in self.blueprints:
|
if blueprint.name in self.blueprints:
|
||||||
assert self.blueprints[blueprint.name] is blueprint, (
|
assert self.blueprints[blueprint.name] is blueprint, (
|
||||||
@@ -656,12 +567,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
# determine if the parameter supplied by the caller
|
# determine if the parameter supplied by the caller
|
||||||
# passes the test in the URL
|
# passes the test in the URL
|
||||||
if param_info.pattern:
|
if param_info.pattern:
|
||||||
pattern = (
|
passes_pattern = param_info.pattern.match(supplied_param)
|
||||||
param_info.pattern[1]
|
|
||||||
if isinstance(param_info.pattern, tuple)
|
|
||||||
else param_info.pattern
|
|
||||||
)
|
|
||||||
passes_pattern = pattern.match(supplied_param)
|
|
||||||
if not passes_pattern:
|
if not passes_pattern:
|
||||||
if param_info.cast != str:
|
if param_info.cast != str:
|
||||||
msg = (
|
msg = (
|
||||||
@@ -669,13 +575,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
f"for parameter `{param_info.name}` does "
|
f"for parameter `{param_info.name}` does "
|
||||||
"not match pattern for type "
|
"not match pattern for type "
|
||||||
f"`{param_info.cast.__name__}`: "
|
f"`{param_info.cast.__name__}`: "
|
||||||
f"{pattern.pattern}"
|
f"{param_info.pattern.pattern}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = (
|
msg = (
|
||||||
f'Value "{supplied_param}" for parameter '
|
f'Value "{supplied_param}" for parameter '
|
||||||
f"`{param_info.name}` does not satisfy "
|
f"`{param_info.name}` does not satisfy "
|
||||||
f"pattern {pattern.pattern}"
|
f"pattern {param_info.pattern.pattern}"
|
||||||
)
|
)
|
||||||
raise URLBuildError(msg)
|
raise URLBuildError(msg)
|
||||||
|
|
||||||
@@ -696,7 +602,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
async def handle_exception(
|
async def handle_exception(
|
||||||
self, request: Request, exception: BaseException
|
self, request: Request, exception: BaseException
|
||||||
): # no cov
|
):
|
||||||
"""
|
"""
|
||||||
A handler that catches specific exceptions and outputs a response.
|
A handler that catches specific exceptions and outputs a response.
|
||||||
|
|
||||||
@@ -706,12 +612,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
:type exception: BaseException
|
:type exception: BaseException
|
||||||
:raises ServerError: response 500
|
:raises ServerError: response 500
|
||||||
"""
|
"""
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.exception",
|
|
||||||
inline=True,
|
|
||||||
context={"request": request, "exception": exception},
|
|
||||||
)
|
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
@@ -758,60 +658,41 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
f"Invalid response type {response!r} (need HTTPResponse)"
|
f"Invalid response type {response!r} (need HTTPResponse)"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_request(self, request: Request): # no cov
|
async def handle_request(self, request: Request):
|
||||||
"""Take a request from the HTTP Server and return a response object
|
"""Take a request from the HTTP Server and return a response object
|
||||||
to be sent back The HTTP Server only expects a response object, so
|
to be sent back The HTTP Server only expects a response object, so
|
||||||
exception handling must be done here
|
exception handling must be done here
|
||||||
|
|
||||||
:param request: HTTP Request object
|
:param request: HTTP Request object
|
||||||
|
:param write_callback: Synchronous response function to be
|
||||||
|
called with the response as the only argument
|
||||||
|
:param stream_callback: Coroutine that handles streaming a
|
||||||
|
StreamingHTTPResponse if produced by the handler.
|
||||||
|
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.handle",
|
|
||||||
inline=True,
|
|
||||||
context={"request": request},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define `response` var here to remove warnings about
|
# Define `response` var here to remove warnings about
|
||||||
# allocation before assignment below.
|
# allocation before assignment below.
|
||||||
response = None
|
response = None
|
||||||
try:
|
try:
|
||||||
|
|
||||||
await self.dispatch(
|
|
||||||
"http.routing.before",
|
|
||||||
inline=True,
|
|
||||||
context={"request": request},
|
|
||||||
)
|
|
||||||
# Fetch handler from router
|
# Fetch handler from router
|
||||||
route, handler, kwargs = self.router.get(
|
route, handler, kwargs = self.router.get(
|
||||||
request.path,
|
request.path, request.method, request.headers.get("host")
|
||||||
request.method,
|
|
||||||
request.headers.getone("host", None),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
request._match_info = {**kwargs}
|
request._match_info = kwargs
|
||||||
request.route = route
|
request.route = route
|
||||||
|
|
||||||
await self.dispatch(
|
|
||||||
"http.routing.after",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"route": route,
|
|
||||||
"kwargs": kwargs,
|
|
||||||
"handler": handler,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
request.stream
|
request.stream.request_body # type: ignore
|
||||||
and request.stream.request_body
|
|
||||||
and not route.ctx.ignore_body
|
and not route.ctx.ignore_body
|
||||||
):
|
):
|
||||||
|
|
||||||
if hasattr(handler, "is_stream"):
|
if hasattr(handler, "is_stream"):
|
||||||
# Streaming handler: lift the size limit
|
# Streaming handler: lift the size limit
|
||||||
request.stream.request_max_size = float("inf")
|
request.stream.request_max_size = float( # type: ignore
|
||||||
|
"inf"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Non-streaming handler: preload body
|
# Non-streaming handler: preload body
|
||||||
await request.receive_body()
|
await request.receive_body()
|
||||||
@@ -838,28 +719,23 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Run response handler
|
# Run response handler
|
||||||
response = handler(request, **request.match_info)
|
response = handler(request, **kwargs)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
|
||||||
if response is not None:
|
if response:
|
||||||
response = await request.respond(response)
|
response = await request.respond(response)
|
||||||
elif not hasattr(handler, "is_websocket"):
|
else:
|
||||||
response = request.stream.response # type: ignore
|
response = request.stream.response # type: ignore
|
||||||
|
|
||||||
# Make sure that response is finished / run StreamingHTTP callback
|
# Make sure that response is finished / run StreamingHTTP callback
|
||||||
|
|
||||||
if isinstance(response, BaseHTTPResponse):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.response",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": response,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await response.send(end_stream=True)
|
await response.send(end_stream=True)
|
||||||
else:
|
else:
|
||||||
if not hasattr(handler, "is_websocket"):
|
try:
|
||||||
|
# Fastest method for checking if the property exists
|
||||||
|
handler.is_websocket # type: ignore
|
||||||
|
except AttributeError:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
f"Invalid response type {response!r} "
|
f"Invalid response type {response!r} "
|
||||||
"(need HTTPResponse)"
|
"(need HTTPResponse)"
|
||||||
@@ -874,11 +750,22 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
async def _websocket_handler(
|
async def _websocket_handler(
|
||||||
self, handler, request, *args, subprotocols=None, **kwargs
|
self, handler, request, *args, subprotocols=None, **kwargs
|
||||||
):
|
):
|
||||||
|
request.app = self
|
||||||
|
if not getattr(handler, "__blueprintname__", False):
|
||||||
|
request._name = handler.__name__
|
||||||
|
else:
|
||||||
|
request._name = (
|
||||||
|
getattr(handler, "__blueprintname__", "") + handler.__name__
|
||||||
|
)
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
if self.asgi:
|
if self.asgi:
|
||||||
ws = request.transport.get_websocket_connection()
|
ws = request.transport.get_websocket_connection()
|
||||||
await ws.accept(subprotocols)
|
|
||||||
else:
|
else:
|
||||||
protocol = request.transport.get_protocol()
|
protocol = request.transport.get_protocol()
|
||||||
|
protocol.app = self
|
||||||
|
|
||||||
ws = await protocol.websocket_handshake(request, subprotocols)
|
ws = await protocol.websocket_handshake(request, subprotocols)
|
||||||
|
|
||||||
# schedule the application handler
|
# schedule the application handler
|
||||||
@@ -886,19 +773,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
# needs to be cancelled due to the server being stopped
|
# needs to be cancelled due to the server being stopped
|
||||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||||
self.websocket_tasks.add(fut)
|
self.websocket_tasks.add(fut)
|
||||||
cancelled = False
|
|
||||||
try:
|
try:
|
||||||
await fut
|
await fut
|
||||||
except Exception as e:
|
|
||||||
self.error_handler.log(request, e)
|
|
||||||
except (CancelledError, ConnectionClosed):
|
except (CancelledError, ConnectionClosed):
|
||||||
cancelled = True
|
pass
|
||||||
finally:
|
finally:
|
||||||
self.websocket_tasks.remove(fut)
|
self.websocket_tasks.remove(fut)
|
||||||
if cancelled:
|
await ws.close()
|
||||||
ws.end_connection(1000)
|
|
||||||
else:
|
|
||||||
await ws.close()
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Testing
|
# Testing
|
||||||
@@ -944,7 +825,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
*,
|
*,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
auto_reload: Optional[bool] = None,
|
auto_reload: Optional[bool] = None,
|
||||||
ssl: Union[Dict[str, str], SSLContext, None] = None,
|
ssl: Union[dict, SSLContext, None] = None,
|
||||||
sock: Optional[socket] = None,
|
sock: Optional[socket] = None,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
protocol: Optional[Type[Protocol]] = None,
|
protocol: Optional[Type[Protocol]] = None,
|
||||||
@@ -953,7 +834,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
loop: None = None,
|
loop: None = None,
|
||||||
reload_dir: Optional[Union[List[str], str]] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run the HTTP Server and listen until keyboard interrupt or term
|
Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
@@ -988,18 +868,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
:type unix: str
|
:type unix: str
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if reload_dir:
|
|
||||||
if isinstance(reload_dir, str):
|
|
||||||
reload_dir = [reload_dir]
|
|
||||||
|
|
||||||
for directory in reload_dir:
|
|
||||||
direc = Path(directory)
|
|
||||||
if not direc.is_dir():
|
|
||||||
logger.warning(
|
|
||||||
f"Directory {directory} could not be located"
|
|
||||||
)
|
|
||||||
self.reload_dirs.add(Path(directory))
|
|
||||||
|
|
||||||
if loop is not None:
|
if loop is not None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"loop is not a valid argument. To use an existing loop, "
|
"loop is not a valid argument. To use an existing loop, "
|
||||||
@@ -1009,9 +877,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if auto_reload or auto_reload is None and debug:
|
if auto_reload or auto_reload is None and debug:
|
||||||
self.auto_reload = True
|
|
||||||
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
|
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
|
||||||
return reloader_helpers.watchdog(1.0, self)
|
return reloader_helpers.watchdog(1.0)
|
||||||
|
|
||||||
if sock is None:
|
if sock is None:
|
||||||
host, port = host or "127.0.0.1", port or 8000
|
host, port = host or "127.0.0.1", port or 8000
|
||||||
@@ -1074,7 +941,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
port: Optional[int] = None,
|
port: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
ssl: Union[Dict[str, str], SSLContext, None] = None,
|
ssl: Union[dict, SSLContext, None] = None,
|
||||||
sock: Optional[socket] = None,
|
sock: Optional[socket] = None,
|
||||||
protocol: Type[Protocol] = None,
|
protocol: Type[Protocol] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
@@ -1146,6 +1013,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
run_async=return_asyncio_server,
|
run_async=return_asyncio_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Trigger before_start events
|
||||||
|
await self.trigger_events(
|
||||||
|
server_settings.get("before_start", []),
|
||||||
|
server_settings.get("loop"),
|
||||||
|
)
|
||||||
main_start = server_settings.pop("main_start", None)
|
main_start = server_settings.pop("main_start", None)
|
||||||
main_stop = server_settings.pop("main_stop", None)
|
main_stop = server_settings.pop("main_stop", None)
|
||||||
if main_start or main_stop:
|
if main_start or main_stop:
|
||||||
@@ -1158,9 +1030,17 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _run_request_middleware(
|
async def trigger_events(self, events, loop):
|
||||||
self, request, request_name=None
|
"""Trigger events (functions or async)
|
||||||
): # no cov
|
:param events: one or more sync or async functions to execute
|
||||||
|
:param loop: event loop
|
||||||
|
"""
|
||||||
|
for event in events:
|
||||||
|
result = event(loop)
|
||||||
|
if isawaitable(result):
|
||||||
|
await result
|
||||||
|
|
||||||
|
async def _run_request_middleware(self, request, request_name=None):
|
||||||
# The if improves speed. I don't know why
|
# The if improves speed. I don't know why
|
||||||
named_middleware = self.named_request_middleware.get(
|
named_middleware = self.named_request_middleware.get(
|
||||||
request_name, deque()
|
request_name, deque()
|
||||||
@@ -1173,67 +1053,25 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
request.request_middleware_started = True
|
request.request_middleware_started = True
|
||||||
|
|
||||||
for middleware in applicable_middleware:
|
for middleware in applicable_middleware:
|
||||||
await self.dispatch(
|
|
||||||
"http.middleware.before",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": None,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
response = middleware(request)
|
response = middleware(request)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
|
||||||
await self.dispatch(
|
|
||||||
"http.middleware.after",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": None,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
return response
|
return response
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _run_response_middleware(
|
async def _run_response_middleware(
|
||||||
self, request, response, request_name=None
|
self, request, response, request_name=None
|
||||||
): # no cov
|
):
|
||||||
named_middleware = self.named_response_middleware.get(
|
named_middleware = self.named_response_middleware.get(
|
||||||
request_name, deque()
|
request_name, deque()
|
||||||
)
|
)
|
||||||
applicable_middleware = self.response_middleware + named_middleware
|
applicable_middleware = self.response_middleware + named_middleware
|
||||||
if applicable_middleware:
|
if applicable_middleware:
|
||||||
for middleware in applicable_middleware:
|
for middleware in applicable_middleware:
|
||||||
await self.dispatch(
|
|
||||||
"http.middleware.before",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": response,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "response"},
|
|
||||||
)
|
|
||||||
|
|
||||||
_response = middleware(request, response)
|
_response = middleware(request, response)
|
||||||
if isawaitable(_response):
|
if isawaitable(_response):
|
||||||
_response = await _response
|
_response = await _response
|
||||||
|
|
||||||
await self.dispatch(
|
|
||||||
"http.middleware.after",
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": _response if _response else response,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "response"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if _response:
|
if _response:
|
||||||
response = _response
|
response = _response
|
||||||
if isinstance(response, BaseHTTPResponse):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
@@ -1259,6 +1097,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
):
|
):
|
||||||
"""Helper function used by `run` and `create_server`."""
|
"""Helper function used by `run` and `create_server`."""
|
||||||
|
|
||||||
|
self.listeners["before_server_start"] = [
|
||||||
|
self.finalize
|
||||||
|
] + self.listeners["before_server_start"]
|
||||||
|
|
||||||
if isinstance(ssl, dict):
|
if isinstance(ssl, dict):
|
||||||
# try common aliaseses
|
# try common aliaseses
|
||||||
cert = ssl.get("cert") or ssl.get("certificate")
|
cert = ssl.get("cert") or ssl.get("certificate")
|
||||||
@@ -1295,6 +1137,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
# Register start/stop events
|
# Register start/stop events
|
||||||
|
|
||||||
for event_name, settings_name, reverse in (
|
for event_name, settings_name, reverse in (
|
||||||
|
("before_server_start", "before_start", False),
|
||||||
|
("after_server_start", "after_start", False),
|
||||||
|
("before_server_stop", "before_stop", True),
|
||||||
|
("after_server_stop", "after_stop", True),
|
||||||
("main_process_start", "main_start", False),
|
("main_process_start", "main_start", False),
|
||||||
("main_process_stop", "main_stop", True),
|
("main_process_stop", "main_stop", True),
|
||||||
):
|
):
|
||||||
@@ -1331,11 +1177,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
else:
|
else:
|
||||||
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
|
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
|
||||||
|
|
||||||
debug_mode = "enabled" if self.debug else "disabled"
|
|
||||||
reload_mode = "enabled" if auto_reload else "disabled"
|
|
||||||
logger.debug(f"Sanic auto-reload: {reload_mode}")
|
|
||||||
logger.debug(f"Sanic debug mode: {debug_mode}")
|
|
||||||
|
|
||||||
return server_settings
|
return server_settings
|
||||||
|
|
||||||
def _build_endpoint_name(self, *parts):
|
def _build_endpoint_name(self, *parts):
|
||||||
@@ -1343,44 +1184,20 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
return ".".join(parts)
|
return ".".join(parts)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _prep_task(cls, task, app, loop):
|
def _loop_add_task(cls, task, app, loop):
|
||||||
if callable(task):
|
if callable(task):
|
||||||
try:
|
try:
|
||||||
task = task(app)
|
loop.create_task(task(app))
|
||||||
except TypeError:
|
except TypeError:
|
||||||
task = task()
|
loop.create_task(task())
|
||||||
|
else:
|
||||||
return task
|
loop.create_task(task)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _loop_add_task(cls, task, app, loop):
|
|
||||||
prepped = cls._prep_task(task, app, loop)
|
|
||||||
loop.create_task(prepped)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _cancel_websocket_tasks(cls, app, loop):
|
def _cancel_websocket_tasks(cls, app, loop):
|
||||||
for task in app.websocket_tasks:
|
for task in app.websocket_tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def dispatch_delayed_tasks(app, loop):
|
|
||||||
for name in app._delayed_tasks:
|
|
||||||
await app.dispatch(name, context={"app": app, "loop": loop})
|
|
||||||
app._delayed_tasks.clear()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def run_delayed_task(app, loop, task):
|
|
||||||
prepped = app._prep_task(task, app, loop)
|
|
||||||
await prepped
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _listener(
|
|
||||||
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
|
||||||
):
|
|
||||||
maybe_coro = listener(app, loop)
|
|
||||||
if maybe_coro and isawaitable(maybe_coro):
|
|
||||||
await maybe_coro
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# ASGI
|
# ASGI
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
@@ -1454,54 +1271,15 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
raise SanicException(f'Sanic app name "{name}" not found.')
|
raise SanicException(f'Sanic app name "{name}" not found.')
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Lifecycle
|
# Static methods
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def finalize(self):
|
@staticmethod
|
||||||
|
async def finalize(app, _):
|
||||||
try:
|
try:
|
||||||
self.router.finalize()
|
app.router.finalize()
|
||||||
|
if app.signal_router.routes:
|
||||||
|
app.signal_router.finalize() # noqa
|
||||||
except FinalizationError as e:
|
except FinalizationError as e:
|
||||||
if not Sanic.test_mode:
|
if not Sanic.test_mode:
|
||||||
raise e
|
raise e # noqa
|
||||||
|
|
||||||
def signalize(self):
|
|
||||||
try:
|
|
||||||
self.signal_router.finalize()
|
|
||||||
except FinalizationError as e:
|
|
||||||
if not Sanic.test_mode:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
async def _startup(self):
|
|
||||||
self.signalize()
|
|
||||||
self.finalize()
|
|
||||||
ErrorHandler.finalize(
|
|
||||||
self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT
|
|
||||||
)
|
|
||||||
TouchUp.run(self)
|
|
||||||
|
|
||||||
async def _server_event(
|
|
||||||
self,
|
|
||||||
concern: str,
|
|
||||||
action: str,
|
|
||||||
loop: Optional[AbstractEventLoop] = None,
|
|
||||||
) -> None:
|
|
||||||
event = f"server.{concern}.{action}"
|
|
||||||
if action not in ("before", "after") or concern not in (
|
|
||||||
"init",
|
|
||||||
"shutdown",
|
|
||||||
):
|
|
||||||
raise SanicException(f"Invalid server event: {event}")
|
|
||||||
logger.debug(f"Triggering server events: {event}")
|
|
||||||
reverse = concern == "shutdown"
|
|
||||||
if loop is None:
|
|
||||||
loop = self.loop
|
|
||||||
await self.dispatch(
|
|
||||||
event,
|
|
||||||
fail_not_found=False,
|
|
||||||
reverse=reverse,
|
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"app": self,
|
|
||||||
"loop": loop,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from inspect import isawaitable
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
@@ -10,27 +11,21 @@ from sanic.exceptions import ServerError
|
|||||||
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.server import ConnInfo
|
from sanic.server import ConnInfo
|
||||||
from sanic.server.websockets.connection import WebSocketConnection
|
from sanic.websocket import WebSocketConnection
|
||||||
|
|
||||||
|
|
||||||
class Lifespan:
|
class Lifespan:
|
||||||
def __init__(self, asgi_app: "ASGIApp") -> None:
|
def __init__(self, asgi_app: "ASGIApp") -> None:
|
||||||
self.asgi_app = asgi_app
|
self.asgi_app = asgi_app
|
||||||
|
|
||||||
if (
|
if "before_server_start" in self.asgi_app.sanic_app.listeners:
|
||||||
"server.init.before"
|
|
||||||
in self.asgi_app.sanic_app.signal_router.name_index
|
|
||||||
):
|
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
'You have set a listener for "before_server_start" '
|
'You have set a listener for "before_server_start" '
|
||||||
"in ASGI mode. "
|
"in ASGI mode. "
|
||||||
"It will be executed as early as possible, but not before "
|
"It will be executed as early as possible, but not before "
|
||||||
"the ASGI server is started."
|
"the ASGI server is started."
|
||||||
)
|
)
|
||||||
if (
|
if "after_server_stop" in self.asgi_app.sanic_app.listeners:
|
||||||
"server.shutdown.after"
|
|
||||||
in self.asgi_app.sanic_app.signal_router.name_index
|
|
||||||
):
|
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
'You have set a listener for "after_server_stop" '
|
'You have set a listener for "after_server_stop" '
|
||||||
"in ASGI mode. "
|
"in ASGI mode. "
|
||||||
@@ -47,9 +42,19 @@ class Lifespan:
|
|||||||
in sequence since the ASGI lifespan protocol only supports a single
|
in sequence since the ASGI lifespan protocol only supports a single
|
||||||
startup event.
|
startup event.
|
||||||
"""
|
"""
|
||||||
await self.asgi_app.sanic_app._startup()
|
self.asgi_app.sanic_app.router.finalize()
|
||||||
await self.asgi_app.sanic_app._server_event("init", "before")
|
if self.asgi_app.sanic_app.signal_router.routes:
|
||||||
await self.asgi_app.sanic_app._server_event("init", "after")
|
self.asgi_app.sanic_app.signal_router.finalize()
|
||||||
|
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||||
|
"before_server_start", []
|
||||||
|
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
|
||||||
|
|
||||||
|
for handler in listeners:
|
||||||
|
response = handler(
|
||||||
|
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||||
|
)
|
||||||
|
if response and isawaitable(response):
|
||||||
|
await response
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -60,8 +65,16 @@ class Lifespan:
|
|||||||
in sequence since the ASGI lifespan protocol only supports a single
|
in sequence since the ASGI lifespan protocol only supports a single
|
||||||
shutdown event.
|
shutdown event.
|
||||||
"""
|
"""
|
||||||
await self.asgi_app.sanic_app._server_event("shutdown", "before")
|
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||||
await self.asgi_app.sanic_app._server_event("shutdown", "after")
|
"before_server_stop", []
|
||||||
|
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
|
||||||
|
|
||||||
|
for handler in listeners:
|
||||||
|
response = handler(
|
||||||
|
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||||
|
)
|
||||||
|
if response and isawaitable(response):
|
||||||
|
await response
|
||||||
|
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||||
@@ -127,6 +140,7 @@ class ASGIApp:
|
|||||||
instance.ws = instance.transport.create_websocket_connection(
|
instance.ws = instance.transport.create_websocket_connection(
|
||||||
send, receive
|
send, receive
|
||||||
)
|
)
|
||||||
|
await instance.ws.accept()
|
||||||
else:
|
else:
|
||||||
raise ServerError("Received unknown ASGI scope")
|
raise ServerError("Received unknown ASGI scope")
|
||||||
|
|
||||||
@@ -194,7 +208,4 @@ class ASGIApp:
|
|||||||
"""
|
"""
|
||||||
Handle the incoming request.
|
Handle the incoming request.
|
||||||
"""
|
"""
|
||||||
try:
|
await self.sanic_app.handle_request(self.request)
|
||||||
await self.sanic_app.handle_request(self.request)
|
|
||||||
except Exception as e:
|
|
||||||
await self.sanic_app.handle_exception(self.request, e)
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from typing import Any, Tuple
|
from typing import Any, Tuple
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.exceptions import SanicException
|
|
||||||
from sanic.mixins.exceptions import ExceptionMixin
|
from sanic.mixins.exceptions import ExceptionMixin
|
||||||
from sanic.mixins.listeners import ListenerMixin
|
from sanic.mixins.listeners import ListenerMixin
|
||||||
from sanic.mixins.middleware import MiddlewareMixin
|
from sanic.mixins.middleware import MiddlewareMixin
|
||||||
@@ -11,9 +8,6 @@ from sanic.mixins.routes import RouteMixin
|
|||||||
from sanic.mixins.signals import SignalMixin
|
from sanic.mixins.signals import SignalMixin
|
||||||
|
|
||||||
|
|
||||||
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$")
|
|
||||||
|
|
||||||
|
|
||||||
class BaseSanic(
|
class BaseSanic(
|
||||||
RouteMixin,
|
RouteMixin,
|
||||||
MiddlewareMixin,
|
MiddlewareMixin,
|
||||||
@@ -23,25 +17,7 @@ class BaseSanic(
|
|||||||
):
|
):
|
||||||
__fake_slots__: Tuple[str, ...]
|
__fake_slots__: Tuple[str, ...]
|
||||||
|
|
||||||
def __init__(self, name: str = None, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> 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__:
|
for base in BaseSanic.__bases__:
|
||||||
base.__init__(self, *args, **kwargs) # type: ignore
|
base.__init__(self, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
@@ -58,9 +34,8 @@ class BaseSanic(
|
|||||||
if name not in self.__fake_slots__:
|
if name not in self.__fake_slots__:
|
||||||
warn(
|
warn(
|
||||||
f"Setting variables on {self.__class__.__name__} instances is "
|
f"Setting variables on {self.__class__.__name__} instances is "
|
||||||
"deprecated and will be removed in version 21.12. You should "
|
"deprecated and will be removed in version 21.9. You should "
|
||||||
f"change your {self.__class__.__name__} instance to use "
|
f"change your {self.__class__.__name__} instance to use "
|
||||||
f"instance.ctx.{name} instead.",
|
f"instance.ctx.{name} instead."
|
||||||
DeprecationWarning,
|
|
||||||
)
|
)
|
||||||
super().__setattr__(name, value)
|
super().__setattr__(name, value)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import MutableSequence
|
from collections.abc import MutableSequence
|
||||||
from functools import partial
|
|
||||||
from typing import TYPE_CHECKING, List, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
|
import sanic
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
@@ -59,20 +58,13 @@ class BlueprintGroup(MutableSequence):
|
|||||||
app.blueprint(bpg)
|
app.blueprint(bpg)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")
|
||||||
"_blueprints",
|
|
||||||
"_url_prefix",
|
|
||||||
"_version",
|
|
||||||
"_strict_slashes",
|
|
||||||
"_version_prefix",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
url_prefix: Optional[str] = None,
|
url_prefix: Optional[str] = None,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version_prefix: str = "/v",
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new Blueprint Group
|
Create a new Blueprint Group
|
||||||
@@ -85,7 +77,6 @@ class BlueprintGroup(MutableSequence):
|
|||||||
self._blueprints: List[Blueprint] = []
|
self._blueprints: List[Blueprint] = []
|
||||||
self._url_prefix = url_prefix
|
self._url_prefix = url_prefix
|
||||||
self._version = version
|
self._version = version
|
||||||
self._version_prefix = version_prefix
|
|
||||||
self._strict_slashes = strict_slashes
|
self._strict_slashes = strict_slashes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -98,7 +89,7 @@ class BlueprintGroup(MutableSequence):
|
|||||||
return self._url_prefix
|
return self._url_prefix
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def blueprints(self) -> List[Blueprint]:
|
def blueprints(self) -> List["sanic.Blueprint"]:
|
||||||
"""
|
"""
|
||||||
Retrieve a list of all the available blueprints under this group.
|
Retrieve a list of all the available blueprints under this group.
|
||||||
|
|
||||||
@@ -125,15 +116,6 @@ class BlueprintGroup(MutableSequence):
|
|||||||
"""
|
"""
|
||||||
return self._strict_slashes
|
return self._strict_slashes
|
||||||
|
|
||||||
@property
|
|
||||||
def version_prefix(self) -> str:
|
|
||||||
"""
|
|
||||||
Version prefix; defaults to ``/v``
|
|
||||||
|
|
||||||
:return: str
|
|
||||||
"""
|
|
||||||
return self._version_prefix
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""
|
"""
|
||||||
Tun the class Blueprint Group into an Iterable item
|
Tun the class Blueprint Group into an Iterable item
|
||||||
@@ -188,37 +170,34 @@ class BlueprintGroup(MutableSequence):
|
|||||||
"""
|
"""
|
||||||
return len(self._blueprints)
|
return len(self._blueprints)
|
||||||
|
|
||||||
def append(self, value: Blueprint) -> None:
|
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
|
||||||
|
"""
|
||||||
|
Sanitize the Blueprint Entity to override the Version and strict slash
|
||||||
|
behaviors as required.
|
||||||
|
|
||||||
|
:param bp: Sanic Blueprint entity Object
|
||||||
|
:return: Modified Blueprint
|
||||||
|
"""
|
||||||
|
if self._url_prefix:
|
||||||
|
merged_prefix = "/".join(
|
||||||
|
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
|
||||||
|
).rstrip("/")
|
||||||
|
bp.url_prefix = f"/{merged_prefix}"
|
||||||
|
for _attr in ["version", "strict_slashes"]:
|
||||||
|
if getattr(bp, _attr) is None:
|
||||||
|
setattr(bp, _attr, getattr(self, _attr))
|
||||||
|
return bp
|
||||||
|
|
||||||
|
def append(self, value: "sanic.Blueprint") -> None:
|
||||||
"""
|
"""
|
||||||
The Abstract class `MutableSequence` leverages this append method to
|
The Abstract class `MutableSequence` leverages this append method to
|
||||||
perform the `BlueprintGroup.append` operation.
|
perform the `BlueprintGroup.append` operation.
|
||||||
:param value: New `Blueprint` object.
|
:param value: New `Blueprint` object.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._blueprints.append(value)
|
self._blueprints.append(self._sanitize_blueprint(bp=value))
|
||||||
|
|
||||||
def exception(self, *exceptions, **kwargs):
|
def insert(self, index: int, item: "sanic.Blueprint") -> None:
|
||||||
"""
|
|
||||||
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
|
The Abstract class `MutableSequence` leverages this insert method to
|
||||||
perform the `BlueprintGroup.append` operation.
|
perform the `BlueprintGroup.append` operation.
|
||||||
@@ -227,7 +206,7 @@ class BlueprintGroup(MutableSequence):
|
|||||||
:param item: New `Blueprint` object.
|
:param item: New `Blueprint` object.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._blueprints.insert(index, item)
|
self._blueprints.insert(index, self._sanitize_blueprint(item))
|
||||||
|
|
||||||
def middleware(self, *args, **kwargs):
|
def middleware(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -251,15 +230,3 @@ class BlueprintGroup(MutableSequence):
|
|||||||
args = list(args)[1:]
|
args = list(args)[1:]
|
||||||
return register_middleware_for_blueprints(fn)
|
return register_middleware_for_blueprints(fn)
|
||||||
return register_middleware_for_blueprints
|
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,7 +3,6 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
|
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ from sanic_routing.route import Route # type: ignore
|
|||||||
from sanic.base import BaseSanic
|
from sanic.base import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.helpers import Default, _default
|
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import (
|
from sanic.models.handler_types import (
|
||||||
ListenerType,
|
ListenerType,
|
||||||
@@ -42,7 +40,7 @@ class Blueprint(BaseSanic):
|
|||||||
:param host: IP Address of FQDN for the sanic server to use.
|
:param host: IP Address of FQDN for the sanic server to use.
|
||||||
:param version: Blueprint Version
|
:param version: Blueprint Version
|
||||||
:param strict_slashes: Enforce the API urls are requested with a
|
:param strict_slashes: Enforce the API urls are requested with a
|
||||||
trailing */*
|
training */*
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__fake_slots__ = (
|
__fake_slots__ = (
|
||||||
@@ -64,23 +62,28 @@ class Blueprint(BaseSanic):
|
|||||||
"strict_slashes",
|
"strict_slashes",
|
||||||
"url_prefix",
|
"url_prefix",
|
||||||
"version",
|
"version",
|
||||||
"version_prefix",
|
|
||||||
"websocket_routes",
|
"websocket_routes",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str = None,
|
name: str,
|
||||||
url_prefix: Optional[str] = None,
|
url_prefix: Optional[str] = None,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version_prefix: str = "/v",
|
|
||||||
):
|
):
|
||||||
super().__init__(name=name)
|
super().__init__()
|
||||||
self.reset()
|
|
||||||
|
self._apps: Set[Sanic] = set()
|
||||||
self.ctx = SimpleNamespace()
|
self.ctx = SimpleNamespace()
|
||||||
|
self.exceptions: List[RouteHandler] = []
|
||||||
self.host = host
|
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.strict_slashes = strict_slashes
|
||||||
self.url_prefix = (
|
self.url_prefix = (
|
||||||
url_prefix[:-1]
|
url_prefix[:-1]
|
||||||
@@ -88,7 +91,7 @@ class Blueprint(BaseSanic):
|
|||||||
else url_prefix
|
else url_prefix
|
||||||
)
|
)
|
||||||
self.version = version
|
self.version = version
|
||||||
self.version_prefix = version_prefix
|
self.websocket_routes: List[Route] = []
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
args = ", ".join(
|
args = ", ".join(
|
||||||
@@ -139,89 +142,8 @@ class Blueprint(BaseSanic):
|
|||||||
kwargs["apply"] = False
|
kwargs["apply"] = False
|
||||||
return super().signal(event, *args, **kwargs)
|
return super().signal(event, *args, **kwargs)
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self._apps: Set[Sanic] = set()
|
|
||||||
self.exceptions: List[RouteHandler] = []
|
|
||||||
self.listeners: Dict[str, List[ListenerType]] = {}
|
|
||||||
self.middlewares: List[MiddlewareType] = []
|
|
||||||
self.routes: List[Route] = []
|
|
||||||
self.statics: List[RouteHandler] = []
|
|
||||||
self.websocket_routes: List[Route] = []
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
: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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
@staticmethod
|
||||||
def group(
|
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
|
||||||
*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",
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Create a list of blueprints, optionally grouping them under a
|
Create a list of blueprints, optionally grouping them under a
|
||||||
general URL prefix.
|
general URL prefix.
|
||||||
@@ -238,6 +160,8 @@ class Blueprint(BaseSanic):
|
|||||||
for i in nested:
|
for i in nested:
|
||||||
if isinstance(i, (list, tuple)):
|
if isinstance(i, (list, tuple)):
|
||||||
yield from chain(i)
|
yield from chain(i)
|
||||||
|
elif isinstance(i, BlueprintGroup):
|
||||||
|
yield from i.blueprints
|
||||||
else:
|
else:
|
||||||
yield i
|
yield i
|
||||||
|
|
||||||
@@ -245,7 +169,6 @@ class Blueprint(BaseSanic):
|
|||||||
url_prefix=url_prefix,
|
url_prefix=url_prefix,
|
||||||
version=version,
|
version=version,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
version_prefix=version_prefix,
|
|
||||||
)
|
)
|
||||||
for bp in chain(blueprints):
|
for bp in chain(blueprints):
|
||||||
bps.append(bp)
|
bps.append(bp)
|
||||||
@@ -263,12 +186,6 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
self._apps.add(app)
|
self._apps.add(app)
|
||||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
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 = []
|
routes = []
|
||||||
middleware = []
|
middleware = []
|
||||||
@@ -283,22 +200,12 @@ class Blueprint(BaseSanic):
|
|||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||||
|
|
||||||
version_prefix = self.version_prefix
|
strict_slashes = (
|
||||||
for prefix in (
|
self.strict_slashes
|
||||||
future.version_prefix,
|
if future.strict_slashes is None
|
||||||
opt_version_prefix,
|
and self.strict_slashes is not None
|
||||||
):
|
else future.strict_slashes
|
||||||
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)
|
name = app._generate_name(future.name)
|
||||||
|
|
||||||
apply_route = FutureRoute(
|
apply_route = FutureRoute(
|
||||||
@@ -308,15 +215,13 @@ class Blueprint(BaseSanic):
|
|||||||
future.host or self.host,
|
future.host or self.host,
|
||||||
strict_slashes,
|
strict_slashes,
|
||||||
future.stream,
|
future.stream,
|
||||||
version,
|
future.version or self.version,
|
||||||
name,
|
name,
|
||||||
future.ignore_body,
|
future.ignore_body,
|
||||||
future.websocket,
|
future.websocket,
|
||||||
future.subprotocols,
|
future.subprotocols,
|
||||||
future.unquote,
|
future.unquote,
|
||||||
future.static,
|
future.static,
|
||||||
version_prefix,
|
|
||||||
error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
route = app._apply_route(apply_route)
|
route = app._apply_route(apply_route)
|
||||||
@@ -335,27 +240,26 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
route_names = [route.name for route in routes if route]
|
route_names = [route.name for route in routes if route]
|
||||||
|
|
||||||
|
# Middleware
|
||||||
if route_names:
|
if route_names:
|
||||||
# Middleware
|
|
||||||
for future in self._future_middleware:
|
for future in self._future_middleware:
|
||||||
middleware.append(app._apply_middleware(future, route_names))
|
middleware.append(app._apply_middleware(future, route_names))
|
||||||
|
|
||||||
# Exceptions
|
# Exceptions
|
||||||
for future in self._future_exceptions:
|
for future in self._future_exceptions:
|
||||||
exception_handlers.append(
|
exception_handlers.append(app._apply_exception_handler(future))
|
||||||
app._apply_exception_handler(future, route_names)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Event listeners
|
# Event listeners
|
||||||
for listener in self._future_listeners:
|
for listener in self._future_listeners:
|
||||||
listeners[listener.event].append(app._apply_listener(listener))
|
listeners[listener.event].append(app._apply_listener(listener))
|
||||||
|
|
||||||
# Signals
|
|
||||||
for signal in self._future_signals:
|
for signal in self._future_signals:
|
||||||
signal.condition.update({"blueprint": self.name})
|
signal.condition.update({"blueprint": self.name})
|
||||||
app._apply_signal(signal)
|
app._apply_signal(signal)
|
||||||
|
|
||||||
self.routes = [route for route in routes if isinstance(route, Route)]
|
self.routes = [route for route in routes if isinstance(route, Route)]
|
||||||
|
|
||||||
|
# Deprecate these in 21.6
|
||||||
self.websocket_routes = [
|
self.websocket_routes = [
|
||||||
route for route in self.routes if route.ctx.websocket
|
route for route in self.routes if route.ctx.websocket
|
||||||
]
|
]
|
||||||
@@ -384,12 +288,3 @@ class Blueprint(BaseSanic):
|
|||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_value(*values):
|
|
||||||
value = values[-1]
|
|
||||||
for v in values:
|
|
||||||
if v is not None:
|
|
||||||
value = v
|
|
||||||
break
|
|
||||||
return value
|
|
||||||
|
|||||||
157
sanic/config.py
157
sanic/config.py
@@ -1,21 +1,11 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from inspect import isclass
|
from inspect import isclass
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
from typing import Any, Union
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
from sanic.errorpages import check_error_format
|
|
||||||
from sanic.http import Http
|
|
||||||
|
|
||||||
from .utils import load_module_from_file_location, str_to_bool
|
from .utils import load_module_from_file_location, str_to_bool
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
|
|
||||||
SANIC_PREFIX = "SANIC_"
|
SANIC_PREFIX = "SANIC_"
|
||||||
BASE_LOGO = """
|
BASE_LOGO = """
|
||||||
|
|
||||||
@@ -25,88 +15,44 @@ BASE_LOGO = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"ACCESS_LOG": True,
|
|
||||||
"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,
|
|
||||||
"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_MAX_SIZE": 100000000, # 100 megabytes
|
||||||
|
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
||||||
|
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
|
||||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||||
"RESPONSE_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_SIZE": 2 ** 20, # 1 megabyte
|
||||||
"WEBSOCKET_PING_INTERVAL": 20,
|
"WEBSOCKET_MAX_QUEUE": 32,
|
||||||
|
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||||
|
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
|
"WEBSOCKET_PING_INTERVAL": 20,
|
||||||
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||||
|
"ACCESS_LOG": True,
|
||||||
|
"FORWARDED_SECRET": None,
|
||||||
|
"REAL_IP_HEADER": None,
|
||||||
|
"PROXIES_COUNT": None,
|
||||||
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
|
"REQUEST_ID_HEADER": "X-Request-ID",
|
||||||
|
"FALLBACK_ERROR_FORMAT": "html",
|
||||||
|
"REGISTER": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Config(dict):
|
class Config(dict):
|
||||||
ACCESS_LOG: bool
|
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||||
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
|
|
||||||
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 {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
|
||||||
self._app = app
|
self.LOGO = BASE_LOGO
|
||||||
self._LOGO = BASE_LOGO
|
|
||||||
|
|
||||||
if keep_alive is not None:
|
if keep_alive is not None:
|
||||||
self.KEEP_ALIVE = keep_alive
|
self.KEEP_ALIVE = keep_alive
|
||||||
|
|
||||||
if env_prefix != SANIC_PREFIX:
|
if load_env:
|
||||||
if env_prefix:
|
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||||
self.load_environment_vars(env_prefix)
|
self.load_environment_vars(prefix=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):
|
def __getattr__(self, attr):
|
||||||
try:
|
try:
|
||||||
@@ -114,61 +60,8 @@ class Config(dict):
|
|||||||
except KeyError as ke:
|
except KeyError as ke:
|
||||||
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
||||||
|
|
||||||
def __setattr__(self, attr, value) -> None:
|
def __setattr__(self, attr, value):
|
||||||
self.update({attr: value})
|
self[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):
|
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,28 +1,2 @@
|
|||||||
from enum import Enum, auto
|
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
|
||||||
|
|
||||||
|
|
||||||
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"
|
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||||
|
|||||||
@@ -340,139 +340,41 @@ RENDERERS_BY_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RENDERERS_BY_CONTENT_TYPE = {
|
RENDERERS_BY_CONTENT_TYPE = {
|
||||||
"text/plain": TextRenderer,
|
|
||||||
"application/json": JSONRenderer,
|
|
||||||
"multipart/form-data": HTMLRenderer,
|
"multipart/form-data": HTMLRenderer,
|
||||||
"text/html": HTMLRenderer,
|
"application/json": JSONRenderer,
|
||||||
|
"text/plain": TextRenderer,
|
||||||
}
|
}
|
||||||
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(
|
def exception_response(
|
||||||
request: Request,
|
request: Request,
|
||||||
exception: Exception,
|
exception: Exception,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
fallback: str,
|
|
||||||
base: t.Type[BaseRenderer],
|
|
||||||
renderer: t.Type[t.Optional[BaseRenderer]] = None,
|
renderer: t.Type[t.Optional[BaseRenderer]] = None,
|
||||||
) -> HTTPResponse:
|
) -> HTTPResponse:
|
||||||
"""
|
"""
|
||||||
Render a response for the default FALLBACK exception handler.
|
Render a response for the default FALLBACK exception handler.
|
||||||
"""
|
"""
|
||||||
content_type = None
|
|
||||||
|
|
||||||
if not renderer:
|
if not renderer:
|
||||||
# Make sure we have something set
|
renderer = HTMLRenderer
|
||||||
renderer = base
|
|
||||||
render_format = fallback
|
|
||||||
|
|
||||||
if request:
|
if request:
|
||||||
# If there is a request, try and get the format
|
if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
|
||||||
# from the route
|
|
||||||
if request.route:
|
|
||||||
try:
|
try:
|
||||||
if request.route.ctx.error_format:
|
renderer = JSONRenderer if request.json else HTMLRenderer
|
||||||
render_format = request.route.ctx.error_format
|
except InvalidUsage:
|
||||||
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
|
renderer = HTMLRenderer
|
||||||
|
|
||||||
# Second, if there is an Accept header, check if
|
content_type, *_ = request.headers.get(
|
||||||
# application/json is an option, or if the content-type
|
"content-type", ""
|
||||||
# is application/json
|
).split(";")
|
||||||
elif (
|
renderer = RENDERERS_BY_CONTENT_TYPE.get(
|
||||||
acceptable
|
content_type, renderer
|
||||||
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:
|
else:
|
||||||
|
render_format = request.app.config.FALLBACK_ERROR_FORMAT
|
||||||
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
|
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)
|
renderer = t.cast(t.Type[BaseRenderer], renderer)
|
||||||
return renderer(request, exception, debug).render()
|
return renderer(request, exception, debug).render()
|
||||||
|
|||||||
@@ -3,22 +3,26 @@ from typing import Optional, Union
|
|||||||
from sanic.helpers import STATUS_CODES
|
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):
|
class SanicException(Exception):
|
||||||
message: str = ""
|
def __init__(self, message, status_code=None, quiet=None):
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
message: Optional[Union[str, bytes]] = None,
|
|
||||||
status_code: Optional[int] = None,
|
|
||||||
quiet: Optional[bool] = None,
|
|
||||||
) -> None:
|
|
||||||
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)
|
super().__init__(message)
|
||||||
|
|
||||||
if status_code is not None:
|
if status_code is not None:
|
||||||
@@ -29,45 +33,45 @@ class SanicException(Exception):
|
|||||||
self.quiet = True
|
self.quiet = True
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(404)
|
||||||
class NotFound(SanicException):
|
class NotFound(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 404 Not Found
|
**Status**: 404 Not Found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 404
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(400)
|
||||||
class InvalidUsage(SanicException):
|
class InvalidUsage(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 400 Bad Request
|
**Status**: 400 Bad Request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 400
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(405)
|
||||||
class MethodNotSupported(SanicException):
|
class MethodNotSupported(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 405 Method Not Allowed
|
**Status**: 405 Method Not Allowed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 405
|
|
||||||
quiet = True
|
|
||||||
|
|
||||||
def __init__(self, message, method, allowed_methods):
|
def __init__(self, message, method, allowed_methods):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.headers = {"Allow": ", ".join(allowed_methods)}
|
self.headers = {"Allow": ", ".join(allowed_methods)}
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(500)
|
||||||
class ServerError(SanicException):
|
class ServerError(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 500 Internal Server Error
|
**Status**: 500 Internal Server Error
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 500
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(503)
|
||||||
class ServiceUnavailable(SanicException):
|
class ServiceUnavailable(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 503 Service Unavailable
|
**Status**: 503 Service Unavailable
|
||||||
@@ -76,8 +80,7 @@ class ServiceUnavailable(SanicException):
|
|||||||
down for maintenance). Generally, this is a temporary state.
|
down for maintenance). Generally, this is a temporary state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 503
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
class URLBuildError(ServerError):
|
class URLBuildError(ServerError):
|
||||||
@@ -85,7 +88,7 @@ class URLBuildError(ServerError):
|
|||||||
**Status**: 500 Internal Server Error
|
**Status**: 500 Internal Server Error
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 500
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileNotFound(NotFound):
|
class FileNotFound(NotFound):
|
||||||
@@ -99,6 +102,7 @@ class FileNotFound(NotFound):
|
|||||||
self.relative_url = relative_url
|
self.relative_url = relative_url
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(408)
|
||||||
class RequestTimeout(SanicException):
|
class RequestTimeout(SanicException):
|
||||||
"""The Web server (running the Web site) thinks that there has been too
|
"""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
|
long an interval of time between 1) the establishment of an IP
|
||||||
@@ -108,17 +112,16 @@ class RequestTimeout(SanicException):
|
|||||||
server has 'timed out' on that particular socket connection.
|
server has 'timed out' on that particular socket connection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 408
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(413)
|
||||||
class PayloadTooLarge(SanicException):
|
class PayloadTooLarge(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 413 Payload Too Large
|
**Status**: 413 Payload Too Large
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 413
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
class HeaderNotFound(InvalidUsage):
|
class HeaderNotFound(InvalidUsage):
|
||||||
@@ -126,42 +129,36 @@ class HeaderNotFound(InvalidUsage):
|
|||||||
**Status**: 400 Bad Request
|
**Status**: 400 Bad Request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
class InvalidHeader(InvalidUsage):
|
|
||||||
"""
|
|
||||||
**Status**: 400 Bad Request
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(416)
|
||||||
class ContentRangeError(SanicException):
|
class ContentRangeError(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 416 Range Not Satisfiable
|
**Status**: 416 Range Not Satisfiable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 416
|
|
||||||
quiet = True
|
|
||||||
|
|
||||||
def __init__(self, message, content_range):
|
def __init__(self, message, content_range):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(417)
|
||||||
class HeaderExpectationFailed(SanicException):
|
class HeaderExpectationFailed(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 417 Expectation Failed
|
**Status**: 417 Expectation Failed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 417
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(403)
|
||||||
class Forbidden(SanicException):
|
class Forbidden(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 403 Forbidden
|
**Status**: 403 Forbidden
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 403
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidRangeType(ContentRangeError):
|
class InvalidRangeType(ContentRangeError):
|
||||||
@@ -169,8 +166,7 @@ class InvalidRangeType(ContentRangeError):
|
|||||||
**Status**: 416 Range Not Satisfiable
|
**Status**: 416 Range Not Satisfiable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 416
|
pass
|
||||||
quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
class PyFileError(Exception):
|
class PyFileError(Exception):
|
||||||
@@ -178,6 +174,7 @@ class PyFileError(Exception):
|
|||||||
super().__init__("could not execute config file %s", file)
|
super().__init__("could not execute config file %s", file)
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(401)
|
||||||
class Unauthorized(SanicException):
|
class Unauthorized(SanicException):
|
||||||
"""
|
"""
|
||||||
**Status**: 401 Unauthorized
|
**Status**: 401 Unauthorized
|
||||||
@@ -213,9 +210,6 @@ class Unauthorized(SanicException):
|
|||||||
realm="Restricted Area")
|
realm="Restricted Area")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 401
|
|
||||||
quiet = True
|
|
||||||
|
|
||||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||||
super().__init__(message, status_code)
|
super().__init__(message, status_code)
|
||||||
|
|
||||||
@@ -237,11 +231,6 @@ class InvalidSignal(SanicException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WebsocketClosed(SanicException):
|
|
||||||
quiet = True
|
|
||||||
message = "Client has closed the websocket connection"
|
|
||||||
|
|
||||||
|
|
||||||
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||||
"""
|
"""
|
||||||
Raise an exception based on SanicException. Returns the HTTP response
|
Raise an exception based on SanicException. Returns the HTTP response
|
||||||
@@ -252,13 +241,9 @@ def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
|||||||
:param status_code: The HTTP status code to return.
|
:param status_code: The HTTP status code to return.
|
||||||
:param message: The HTTP response body. Defaults to the messages in
|
:param message: The HTTP response body. Defaults to the messages in
|
||||||
"""
|
"""
|
||||||
import warnings
|
if message is None:
|
||||||
|
msg: bytes = STATUS_CODES[status_code]
|
||||||
warnings.warn(
|
# These are stored as bytes in the STATUS_CODES dict
|
||||||
"sanic.exceptions.abort has been marked as deprecated, and will be "
|
message = msg.decode("utf8")
|
||||||
"removed in release 21.12.\n To migrate your code, simply replace "
|
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||||
"abort(status_code, msg) with raise SanicException(msg, status_code), "
|
raise sanic_exception(message=message, status_code=status_code)
|
||||||
"or even better, raise an appropriate SanicException subclass."
|
|
||||||
)
|
|
||||||
|
|
||||||
raise SanicException(message=message, status_code=status_code)
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
from inspect import signature
|
from traceback import format_exc
|
||||||
from typing import Dict, List, Optional, Tuple, Type
|
|
||||||
|
|
||||||
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
|
from sanic.errorpages import exception_response
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
ContentRangeError,
|
ContentRangeError,
|
||||||
HeaderNotFound,
|
HeaderNotFound,
|
||||||
InvalidRangeType,
|
InvalidRangeType,
|
||||||
)
|
)
|
||||||
from sanic.log import error_logger
|
from sanic.log import logger
|
||||||
from sanic.models.handler_types import RouteHandler
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
@@ -25,54 +23,16 @@ class ErrorHandler:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Beginning in v22.3, the base renderer will be TextRenderer
|
handlers = None
|
||||||
def __init__(
|
cached_handlers = None
|
||||||
self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer
|
_missing = object()
|
||||||
):
|
|
||||||
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
|
def __init__(self):
|
||||||
self.cached_handlers: Dict[
|
self.handlers = []
|
||||||
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
self.cached_handlers = {}
|
||||||
] = {}
|
|
||||||
self.debug = False
|
self.debug = False
|
||||||
self.fallback = fallback
|
|
||||||
self.base = base
|
|
||||||
|
|
||||||
@classmethod
|
def add(self, exception, handler):
|
||||||
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:
|
|
||||||
error_logger.warning(
|
|
||||||
DeprecationWarning(
|
|
||||||
"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."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
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.
|
Add a new exception handler to an already existing handler object.
|
||||||
|
|
||||||
@@ -85,16 +45,9 @@ class ErrorHandler:
|
|||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# self.handlers is deprecated and will be removed in version 22.3
|
|
||||||
self.handlers.append((exception, handler))
|
self.handlers.append((exception, handler))
|
||||||
|
|
||||||
if route_names:
|
def lookup(self, exception):
|
||||||
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
|
Lookup the existing instance of :class:`ErrorHandler` and fetch the
|
||||||
registered handler for a specific type of exception.
|
registered handler for a specific type of exception.
|
||||||
@@ -108,32 +61,16 @@ class ErrorHandler:
|
|||||||
|
|
||||||
:return: Registered function if found ``None`` otherwise
|
:return: Registered function if found ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
exception_class = type(exception)
|
handler = self.cached_handlers.get(type(exception), self._missing)
|
||||||
|
if handler is self._missing:
|
||||||
for name in (route_name, None):
|
for exception_class, handler in self.handlers:
|
||||||
exception_key = (exception_class, name)
|
if isinstance(exception, exception_class):
|
||||||
handler = self.cached_handlers.get(exception_key)
|
self.cached_handlers[type(exception)] = handler
|
||||||
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
|
return handler
|
||||||
|
self.cached_handlers[type(exception)] = None
|
||||||
if ancestor is BaseException:
|
handler = None
|
||||||
break
|
|
||||||
self.cached_handlers[(exception_class, route_name)] = None
|
|
||||||
handler = None
|
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
_lookup = _full_lookup
|
|
||||||
|
|
||||||
def response(self, request, exception):
|
def response(self, request, exception):
|
||||||
"""Fetches and executes an exception handler and returns a response
|
"""Fetches and executes an exception handler and returns a response
|
||||||
object
|
object
|
||||||
@@ -148,8 +85,7 @@ class ErrorHandler:
|
|||||||
:return: Wrap the return value obtained from :func:`default`
|
:return: Wrap the return value obtained from :func:`default`
|
||||||
or registered handler for that type of exception.
|
or registered handler for that type of exception.
|
||||||
"""
|
"""
|
||||||
route_name = request.name if request else None
|
handler = self.lookup(exception)
|
||||||
handler = self._lookup(exception, route_name)
|
|
||||||
response = None
|
response = None
|
||||||
try:
|
try:
|
||||||
if handler:
|
if handler:
|
||||||
@@ -157,6 +93,7 @@ class ErrorHandler:
|
|||||||
if response is None:
|
if response is None:
|
||||||
response = self.default(request, exception)
|
response = self.default(request, exception)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
self.log(format_exc())
|
||||||
try:
|
try:
|
||||||
url = repr(request.url)
|
url = repr(request.url)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -164,7 +101,7 @@ class ErrorHandler:
|
|||||||
response_message = (
|
response_message = (
|
||||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||||
)
|
)
|
||||||
error_logger.exception(response_message, handler.__name__, url)
|
logger.exception(response_message, handler.__name__, url)
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
return text(response_message % (handler.__name__, url), 500)
|
return text(response_message % (handler.__name__, url), 500)
|
||||||
@@ -172,6 +109,11 @@ class ErrorHandler:
|
|||||||
return text("An error occurred while handling an error", 500)
|
return text("An error occurred while handling an error", 500)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def log(self, message, level="error"):
|
||||||
|
"""
|
||||||
|
Deprecated, do not use.
|
||||||
|
"""
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
"""
|
"""
|
||||||
Provide a default behavior for the objects of :class:`ErrorHandler`.
|
Provide a default behavior for the objects of :class:`ErrorHandler`.
|
||||||
@@ -187,17 +129,6 @@ class ErrorHandler:
|
|||||||
:class:`Exception`
|
:class:`Exception`
|
||||||
:return:
|
: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)
|
quiet = getattr(exception, "quiet", False)
|
||||||
if quiet is False:
|
if quiet is False:
|
||||||
try:
|
try:
|
||||||
@@ -205,9 +136,10 @@ class ErrorHandler:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
url = "unknown"
|
url = "unknown"
|
||||||
|
|
||||||
error_logger.exception(
|
self.log(format_exc())
|
||||||
"Exception occurred while handling uri: %s", url
|
logger.exception("Exception occurred while handling uri: %s", url)
|
||||||
)
|
|
||||||
|
return exception_response(request, exception, self.debug)
|
||||||
|
|
||||||
|
|
||||||
class ContentRangeHandler:
|
class ContentRangeHandler:
|
||||||
@@ -233,7 +165,7 @@ class ContentRangeHandler:
|
|||||||
|
|
||||||
def __init__(self, request, stats):
|
def __init__(self, request, stats):
|
||||||
self.total = stats.st_size
|
self.total = stats.st_size
|
||||||
_range = request.headers.getone("range", None)
|
_range = request.headers.get("Range")
|
||||||
if _range is None:
|
if _range is None:
|
||||||
raise HeaderNotFound("Range Header Not Found")
|
raise HeaderNotFound("Range Header Not Found")
|
||||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||||
|
|||||||
204
sanic/headers.py
204
sanic/headers.py
@@ -1,11 +1,8 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from sanic.exceptions import InvalidHeader
|
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
|
|
||||||
@@ -33,175 +30,6 @@ _host_re = re.compile(
|
|||||||
# For more information, consult ../tests/test_requests.py
|
# 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]:
|
def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||||
"""Parse content-type and content-disposition header values.
|
"""Parse content-type and content-disposition header values.
|
||||||
|
|
||||||
@@ -274,7 +102,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
|||||||
"""Parse traditional proxy headers."""
|
"""Parse traditional proxy headers."""
|
||||||
real_ip_header = config.REAL_IP_HEADER
|
real_ip_header = config.REAL_IP_HEADER
|
||||||
proxies_count = config.PROXIES_COUNT
|
proxies_count = config.PROXIES_COUNT
|
||||||
addr = real_ip_header and headers.getone(real_ip_header, None)
|
addr = real_ip_header and headers.get(real_ip_header)
|
||||||
if not addr and proxies_count:
|
if not addr and proxies_count:
|
||||||
assert proxies_count > 0
|
assert proxies_count > 0
|
||||||
try:
|
try:
|
||||||
@@ -303,7 +131,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
|
|||||||
("port", "x-forwarded-port"),
|
("port", "x-forwarded-port"),
|
||||||
("path", "x-forwarded-path"),
|
("path", "x-forwarded-path"),
|
||||||
):
|
):
|
||||||
yield key, headers.getone(header, None)
|
yield key, headers.get(header)
|
||||||
|
|
||||||
return fwd_normalize(options())
|
return fwd_normalize(options())
|
||||||
|
|
||||||
@@ -366,31 +194,3 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
|||||||
ret += b"%b: %b\r\n" % h
|
ret += b"%b: %b\r\n" % h
|
||||||
ret += b"\r\n"
|
ret += b"\r\n"
|
||||||
return ret
|
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)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -155,17 +155,3 @@ def import_string(module_name, package=None):
|
|||||||
if ismodule(obj):
|
if ismodule(obj):
|
||||||
return obj
|
return 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()
|
|
||||||
|
|||||||
124
sanic/http.py
124
sanic/http.py
@@ -20,8 +20,7 @@ from sanic.exceptions import (
|
|||||||
)
|
)
|
||||||
from sanic.headers import format_http1_response
|
from sanic.headers import format_http1_response
|
||||||
from sanic.helpers import has_message_body
|
from sanic.helpers import has_message_body
|
||||||
from sanic.log import access_logger, error_logger, logger
|
from sanic.log import access_logger, logger
|
||||||
from sanic.touchup import TouchUpMeta
|
|
||||||
|
|
||||||
|
|
||||||
class Stage(Enum):
|
class Stage(Enum):
|
||||||
@@ -46,7 +45,7 @@ class Stage(Enum):
|
|||||||
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
||||||
|
|
||||||
|
|
||||||
class Http(metaclass=TouchUpMeta):
|
class Http:
|
||||||
"""
|
"""
|
||||||
Internal helper for managing the HTTP request/response cycle
|
Internal helper for managing the HTTP request/response cycle
|
||||||
|
|
||||||
@@ -65,18 +64,9 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
:raises RuntimeError:
|
:raises RuntimeError:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
HEADER_CEILING = 16_384
|
|
||||||
HEADER_MAX_SIZE = 0
|
|
||||||
|
|
||||||
__touchup__ = (
|
|
||||||
"http1_request_header",
|
|
||||||
"http1_response_header",
|
|
||||||
"read",
|
|
||||||
)
|
|
||||||
__slots__ = [
|
__slots__ = [
|
||||||
"_send",
|
"_send",
|
||||||
"_receive_more",
|
"_receive_more",
|
||||||
"dispatch",
|
|
||||||
"recv_buffer",
|
"recv_buffer",
|
||||||
"protocol",
|
"protocol",
|
||||||
"expecting_continue",
|
"expecting_continue",
|
||||||
@@ -102,23 +92,19 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
self._receive_more = protocol.receive_more
|
self._receive_more = protocol.receive_more
|
||||||
self.recv_buffer = protocol.recv_buffer
|
self.recv_buffer = protocol.recv_buffer
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.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.expecting_continue: bool = False
|
||||||
self.head_only = None
|
self.stage: Stage = Stage.IDLE
|
||||||
self.request_body = None
|
self.request_body = None
|
||||||
self.request_bytes = None
|
self.request_bytes = None
|
||||||
self.request_bytes_left = None
|
self.request_bytes_left = None
|
||||||
self.request_max_size = self.protocol.request_max_size
|
self.request_max_size = protocol.request_max_size
|
||||||
|
self.keep_alive = True
|
||||||
|
self.head_only = None
|
||||||
self.request: Request = None
|
self.request: Request = None
|
||||||
self.response: BaseHTTPResponse = None
|
self.response: BaseHTTPResponse = None
|
||||||
self.upgrade_websocket = False
|
self.exception = None
|
||||||
self.url = None
|
self.url = None
|
||||||
|
self.upgrade_websocket = False
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""Test if request handling is in progress"""
|
"""Test if request handling is in progress"""
|
||||||
@@ -128,20 +114,14 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
HTTP 1.1 connection handler
|
HTTP 1.1 connection handler
|
||||||
"""
|
"""
|
||||||
# Handle requests while the connection stays reusable
|
while True: # As long as connection stays keep-alive
|
||||||
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:
|
try:
|
||||||
# Receive and handle a request
|
# Receive and handle a request
|
||||||
|
self.stage = Stage.REQUEST
|
||||||
self.response_func = self.http1_response_header
|
self.response_func = self.http1_response_header
|
||||||
|
|
||||||
await self.http1_request_header()
|
await self.http1_request_header()
|
||||||
|
|
||||||
self.stage = Stage.HANDLER
|
|
||||||
self.request.conn_info = self.protocol.conn_info
|
self.request.conn_info = self.protocol.conn_info
|
||||||
await self.protocol.request_handler(self.request)
|
await self.protocol.request_handler(self.request)
|
||||||
|
|
||||||
@@ -153,12 +133,6 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
await self.response.send(end_stream=True)
|
await self.response.send(end_stream=True)
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
# Write an appropriate response before exiting
|
# 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")
|
e = self.exception or ServiceUnavailable("Cancelled")
|
||||||
self.exception = None
|
self.exception = None
|
||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
@@ -170,11 +144,8 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
# Try to consume any remaining request body
|
# Try to consume any remaining request body
|
||||||
if self.request_body:
|
if self.request_body:
|
||||||
if self.response and 200 <= self.response.status < 300:
|
if self.response and 200 <= self.response.status < 300:
|
||||||
error_logger.error(f"{self.request} body not consumed.")
|
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:
|
try:
|
||||||
async for _ in self:
|
async for _ in self:
|
||||||
pass
|
pass
|
||||||
@@ -186,16 +157,19 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
await sleep(0.001)
|
await sleep(0.001)
|
||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
|
|
||||||
# Clean up to free memory and for the next request
|
# Exit and disconnect if no more requests can be taken
|
||||||
if self.request:
|
if self.stage is not Stage.IDLE or not self.keep_alive:
|
||||||
self.request.stream = None
|
break
|
||||||
if self.response:
|
|
||||||
self.response.stream = None
|
|
||||||
|
|
||||||
async def http1_request_header(self): # no cov
|
# Wait for next request
|
||||||
|
if not self.recv_buffer:
|
||||||
|
await self._receive_more()
|
||||||
|
|
||||||
|
async def http1_request_header(self):
|
||||||
"""
|
"""
|
||||||
Receive and parse request header into self.request.
|
Receive and parse request header into self.request.
|
||||||
"""
|
"""
|
||||||
|
HEADER_MAX_SIZE = min(8192, self.request_max_size)
|
||||||
# Receive until full header is in buffer
|
# Receive until full header is in buffer
|
||||||
buf = self.recv_buffer
|
buf = self.recv_buffer
|
||||||
pos = 0
|
pos = 0
|
||||||
@@ -206,12 +180,12 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
break
|
break
|
||||||
|
|
||||||
pos = max(0, len(buf) - 3)
|
pos = max(0, len(buf) - 3)
|
||||||
if pos >= self.HEADER_MAX_SIZE:
|
if pos >= HEADER_MAX_SIZE:
|
||||||
break
|
break
|
||||||
|
|
||||||
await self._receive_more()
|
await self._receive_more()
|
||||||
|
|
||||||
if pos >= self.HEADER_MAX_SIZE:
|
if pos >= HEADER_MAX_SIZE:
|
||||||
raise PayloadTooLarge("Request header exceeds the size limit")
|
raise PayloadTooLarge("Request header exceeds the size limit")
|
||||||
|
|
||||||
# Parse header content
|
# Parse header content
|
||||||
@@ -221,12 +195,6 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
reqline, *split_headers = raw_headers.split("\r\n")
|
reqline, *split_headers = raw_headers.split("\r\n")
|
||||||
method, self.url, protocol = reqline.split(" ")
|
method, self.url, protocol = reqline.split(" ")
|
||||||
|
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.read_head",
|
|
||||||
inline=True,
|
|
||||||
context={"head": bytes(head)},
|
|
||||||
)
|
|
||||||
|
|
||||||
if protocol == "HTTP/1.1":
|
if protocol == "HTTP/1.1":
|
||||||
self.keep_alive = True
|
self.keep_alive = True
|
||||||
elif protocol == "HTTP/1.0":
|
elif protocol == "HTTP/1.0":
|
||||||
@@ -251,9 +219,7 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
raise InvalidUsage("Bad Request")
|
raise InvalidUsage("Bad Request")
|
||||||
|
|
||||||
headers_instance = Header(headers)
|
headers_instance = Header(headers)
|
||||||
self.upgrade_websocket = (
|
self.upgrade_websocket = headers_instance.get("upgrade") == "websocket"
|
||||||
headers_instance.getone("upgrade", "").lower() == "websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prepare a Request object
|
# Prepare a Request object
|
||||||
request = self.protocol.request_class(
|
request = self.protocol.request_class(
|
||||||
@@ -265,17 +231,12 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
transport=self.protocol.transport,
|
transport=self.protocol.transport,
|
||||||
app=self.protocol.app,
|
app=self.protocol.app,
|
||||||
)
|
)
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.request",
|
|
||||||
inline=True,
|
|
||||||
context={"request": request},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prepare for request body
|
# Prepare for request body
|
||||||
self.request_bytes_left = self.request_bytes = 0
|
self.request_bytes_left = self.request_bytes = 0
|
||||||
if request_body:
|
if request_body:
|
||||||
headers = request.headers
|
headers = request.headers
|
||||||
expect = headers.getone("expect", None)
|
expect = headers.get("expect")
|
||||||
|
|
||||||
if expect is not None:
|
if expect is not None:
|
||||||
if expect.lower() == "100-continue":
|
if expect.lower() == "100-continue":
|
||||||
@@ -283,7 +244,7 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
else:
|
else:
|
||||||
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
||||||
|
|
||||||
if headers.getone("transfer-encoding", None) == "chunked":
|
if headers.get("transfer-encoding") == "chunked":
|
||||||
self.request_body = "chunked"
|
self.request_body = "chunked"
|
||||||
pos -= 2 # One CRLF stays in buffer
|
pos -= 2 # One CRLF stays in buffer
|
||||||
else:
|
else:
|
||||||
@@ -294,12 +255,13 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
# Remove header and its trailing CRLF
|
# Remove header and its trailing CRLF
|
||||||
del buf[: pos + 4]
|
del buf[: pos + 4]
|
||||||
|
self.stage = Stage.HANDLER
|
||||||
self.request, request.stream = request, self
|
self.request, request.stream = request, self
|
||||||
self.protocol.state["requests_count"] += 1
|
self.protocol.state["requests_count"] += 1
|
||||||
|
|
||||||
async def http1_response_header(
|
async def http1_response_header(
|
||||||
self, data: bytes, end_stream: bool
|
self, data: bytes, end_stream: bool
|
||||||
) -> None: # no cov
|
) -> None:
|
||||||
res = self.response
|
res = self.response
|
||||||
|
|
||||||
# Compatibility with simple response body
|
# Compatibility with simple response body
|
||||||
@@ -471,8 +433,8 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
"request": "nil",
|
"request": "nil",
|
||||||
}
|
}
|
||||||
if req is not None:
|
if req is not None:
|
||||||
if req.remote_addr or req.ip:
|
if req.ip:
|
||||||
extra["host"] = f"{req.remote_addr or req.ip}:{req.port}"
|
extra["host"] = f"{req.ip}:{req.port}"
|
||||||
extra["request"] = f"{req.method} {req.url}"
|
extra["request"] = f"{req.method} {req.url}"
|
||||||
access_logger.info("", extra=extra)
|
access_logger.info("", extra=extra)
|
||||||
|
|
||||||
@@ -488,7 +450,7 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
if data:
|
if data:
|
||||||
yield data
|
yield data
|
||||||
|
|
||||||
async def read(self) -> Optional[bytes]: # no cov
|
async def read(self) -> Optional[bytes]:
|
||||||
"""
|
"""
|
||||||
Read some bytes of request body.
|
Read some bytes of request body.
|
||||||
"""
|
"""
|
||||||
@@ -520,6 +482,8 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
raise InvalidUsage("Bad chunked encoding")
|
raise InvalidUsage("Bad chunked encoding")
|
||||||
|
|
||||||
|
del buf[: pos + 2]
|
||||||
|
|
||||||
if size <= 0:
|
if size <= 0:
|
||||||
self.request_body = None
|
self.request_body = None
|
||||||
|
|
||||||
@@ -527,17 +491,8 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
raise InvalidUsage("Bad chunked encoding")
|
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
|
return None
|
||||||
|
|
||||||
# Remove CRLF, chunk size and the CRLF that follows
|
|
||||||
del buf[: pos + 2]
|
|
||||||
|
|
||||||
self.request_bytes_left = size
|
self.request_bytes_left = size
|
||||||
self.request_bytes += size
|
self.request_bytes += size
|
||||||
|
|
||||||
@@ -562,12 +517,6 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
self.request_bytes_left -= size
|
self.request_bytes_left -= size
|
||||||
|
|
||||||
await self.dispatch(
|
|
||||||
"http.lifecycle.read_body",
|
|
||||||
inline=True,
|
|
||||||
context={"body": data},
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# Response methods
|
# Response methods
|
||||||
@@ -590,10 +539,3 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
@property
|
@property
|
||||||
def send(self):
|
def send(self):
|
||||||
return self.response_func
|
return self.response_func
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_header_max_size(cls, *sizes: int):
|
|
||||||
cls.HEADER_MAX_SIZE = min(
|
|
||||||
*sizes,
|
|
||||||
cls.HEADER_CEILING,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List, Optional, Union
|
from typing import Any, Callable, Coroutine, List, Optional, Union
|
||||||
|
|
||||||
from sanic.models.futures import FutureListener
|
from sanic.models.futures import FutureListener
|
||||||
from sanic.models.handler_types import ListenerType
|
|
||||||
|
|
||||||
|
|
||||||
class ListenerEvent(str, Enum):
|
class ListenerEvent(str, Enum):
|
||||||
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
BEFORE_SERVER_START = "server.init.before"
|
BEFORE_SERVER_START = auto()
|
||||||
AFTER_SERVER_START = "server.init.after"
|
AFTER_SERVER_START = auto()
|
||||||
BEFORE_SERVER_STOP = "server.shutdown.before"
|
BEFORE_SERVER_STOP = auto()
|
||||||
AFTER_SERVER_STOP = "server.shutdown.after"
|
AFTER_SERVER_STOP = auto()
|
||||||
MAIN_PROCESS_START = auto()
|
MAIN_PROCESS_START = auto()
|
||||||
MAIN_PROCESS_STOP = auto()
|
MAIN_PROCESS_STOP = auto()
|
||||||
|
|
||||||
@@ -27,14 +26,16 @@ class ListenerMixin:
|
|||||||
|
|
||||||
def listener(
|
def listener(
|
||||||
self,
|
self,
|
||||||
listener_or_event: Union[ListenerType, str],
|
listener_or_event: Union[
|
||||||
|
Callable[..., Coroutine[Any, Any, None]], str
|
||||||
|
],
|
||||||
event_or_none: Optional[str] = None,
|
event_or_none: Optional[str] = None,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a listener from a decorated function.
|
Create a listener from a decorated function.
|
||||||
|
|
||||||
To be used as a decorator:
|
To be used as a deocrator:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@@ -62,20 +63,20 @@ class ListenerMixin:
|
|||||||
else:
|
else:
|
||||||
return partial(register_listener, event=listener_or_event)
|
return partial(register_listener, event=listener_or_event)
|
||||||
|
|
||||||
def main_process_start(self, listener: ListenerType) -> ListenerType:
|
def main_process_start(self, listener):
|
||||||
return self.listener(listener, "main_process_start")
|
return self.listener(listener, "main_process_start")
|
||||||
|
|
||||||
def main_process_stop(self, listener: ListenerType) -> ListenerType:
|
def main_process_stop(self, listener):
|
||||||
return self.listener(listener, "main_process_stop")
|
return self.listener(listener, "main_process_stop")
|
||||||
|
|
||||||
def before_server_start(self, listener: ListenerType) -> ListenerType:
|
def before_server_start(self, listener):
|
||||||
return self.listener(listener, "before_server_start")
|
return self.listener(listener, "before_server_start")
|
||||||
|
|
||||||
def after_server_start(self, listener: ListenerType) -> ListenerType:
|
def after_server_start(self, listener):
|
||||||
return self.listener(listener, "after_server_start")
|
return self.listener(listener, "after_server_start")
|
||||||
|
|
||||||
def before_server_stop(self, listener: ListenerType) -> ListenerType:
|
def before_server_stop(self, listener):
|
||||||
return self.listener(listener, "before_server_stop")
|
return self.listener(listener, "before_server_stop")
|
||||||
|
|
||||||
def after_server_stop(self, listener: ListenerType) -> ListenerType:
|
def after_server_stop(self, listener):
|
||||||
return self.listener(listener, "after_server_stop")
|
return self.listener(listener, "after_server_stop")
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
from ast import NodeVisitor, Return, parse
|
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from inspect import getsource, signature
|
from inspect import signature
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from re import sub
|
from re import sub
|
||||||
from textwrap import dedent
|
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
|
from typing import Iterable, List, Optional, Set, Union
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic.compat import stat_async
|
from sanic.compat import stat_async
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
||||||
from sanic.errorpages import RESPONSE_MAPPING
|
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
ContentRangeError,
|
ContentRangeError,
|
||||||
FileNotFound,
|
FileNotFound,
|
||||||
@@ -24,22 +21,15 @@ from sanic.exceptions import (
|
|||||||
from sanic.handlers import ContentRangeHandler
|
from sanic.handlers import ContentRangeHandler
|
||||||
from sanic.log import error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import RouteHandler
|
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
from sanic.response import HTTPResponse, file, file_stream
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
RouteWrapper = Callable[
|
|
||||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin:
|
class RouteMixin:
|
||||||
name: str
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_routes: Set[FutureRoute] = set()
|
self._future_routes: Set[FutureRoute] = set()
|
||||||
self._future_statics: Set[FutureStatic] = set()
|
self._future_statics: Set[FutureStatic] = set()
|
||||||
|
self.name = ""
|
||||||
self.strict_slashes: Optional[bool] = False
|
self.strict_slashes: Optional[bool] = False
|
||||||
|
|
||||||
def _apply_route(self, route: FutureRoute) -> List[Route]:
|
def _apply_route(self, route: FutureRoute) -> List[Route]:
|
||||||
@@ -63,9 +53,7 @@ class RouteMixin:
|
|||||||
websocket: bool = False,
|
websocket: bool = False,
|
||||||
unquote: bool = False,
|
unquote: bool = False,
|
||||||
static: bool = False,
|
static: bool = False,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a route
|
Decorate a function to be registered as a route
|
||||||
|
|
||||||
@@ -78,8 +66,6 @@ class RouteMixin:
|
|||||||
:param name: user defined route name for url_for
|
:param name: user defined route name for url_for
|
||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests)
|
body (eg. GET requests)
|
||||||
:param version_prefix: URL path that should be before the version
|
|
||||||
value; default: ``/v``
|
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -106,8 +92,6 @@ class RouteMixin:
|
|||||||
nonlocal subprotocols
|
nonlocal subprotocols
|
||||||
nonlocal websocket
|
nonlocal websocket
|
||||||
nonlocal static
|
nonlocal static
|
||||||
nonlocal version_prefix
|
|
||||||
nonlocal error_format
|
|
||||||
|
|
||||||
if isinstance(handler, tuple):
|
if isinstance(handler, tuple):
|
||||||
# if a handler fn is already wrapped in a route, the handler
|
# if a handler fn is already wrapped in a route, the handler
|
||||||
@@ -126,15 +110,9 @@ class RouteMixin:
|
|||||||
"Expected either string or Iterable of host strings, "
|
"Expected either string or Iterable of host strings, "
|
||||||
"not %s" % host
|
"not %s" % host
|
||||||
)
|
)
|
||||||
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":
|
if isinstance(subprotocols, (list, tuple, set)):
|
||||||
error_format = self._determine_error_format(handler)
|
subprotocols = frozenset(subprotocols)
|
||||||
|
|
||||||
route = FutureRoute(
|
route = FutureRoute(
|
||||||
handler,
|
handler,
|
||||||
@@ -150,8 +128,6 @@ class RouteMixin:
|
|||||||
subprotocols,
|
subprotocols,
|
||||||
unquote,
|
unquote,
|
||||||
static,
|
static,
|
||||||
version_prefix,
|
|
||||||
error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._future_routes.add(route)
|
self._future_routes.add(route)
|
||||||
@@ -178,15 +154,13 @@ class RouteMixin:
|
|||||||
if apply:
|
if apply:
|
||||||
self._apply_route(route)
|
self._apply_route(route)
|
||||||
|
|
||||||
if static:
|
return route, handler
|
||||||
return route, handler
|
|
||||||
return handler
|
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_route(
|
def add_route(
|
||||||
self,
|
self,
|
||||||
handler: RouteHandler,
|
handler,
|
||||||
uri: str,
|
uri: str,
|
||||||
methods: Iterable[str] = frozenset({"GET"}),
|
methods: Iterable[str] = frozenset({"GET"}),
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
@@ -194,9 +168,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteHandler:
|
|
||||||
"""A helper method to register class instance or
|
"""A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
routes.
|
routes.
|
||||||
@@ -210,8 +182,6 @@ class RouteMixin:
|
|||||||
:param version:
|
:param version:
|
||||||
:param name: user defined route name for url_for
|
:param name: user defined route name for url_for
|
||||||
:param stream: boolean specifying if the handler is a stream handler
|
: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
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
@@ -219,8 +189,7 @@ class RouteMixin:
|
|||||||
methods = set()
|
methods = set()
|
||||||
|
|
||||||
for method in HTTP_METHODS:
|
for method in HTTP_METHODS:
|
||||||
view_class = getattr(handler, "view_class")
|
_handler = getattr(handler.view_class, method.lower(), None)
|
||||||
_handler = getattr(view_class, method.lower(), None)
|
|
||||||
if _handler:
|
if _handler:
|
||||||
methods.add(method)
|
methods.add(method)
|
||||||
if hasattr(_handler, "is_stream"):
|
if hasattr(_handler, "is_stream"):
|
||||||
@@ -245,8 +214,6 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)(handler)
|
)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
@@ -259,9 +226,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **GET** *HTTP* method
|
Add an API URL under the **GET** *HTTP* method
|
||||||
|
|
||||||
@@ -271,8 +236,6 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -283,8 +246,6 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(
|
def post(
|
||||||
@@ -295,9 +256,7 @@ class RouteMixin:
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **POST** *HTTP* method
|
Add an API URL under the **POST** *HTTP* method
|
||||||
|
|
||||||
@@ -307,8 +266,6 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -319,8 +276,6 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def put(
|
def put(
|
||||||
@@ -331,9 +286,7 @@ class RouteMixin:
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PUT** *HTTP* method
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
|
|
||||||
@@ -343,8 +296,6 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -355,8 +306,6 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def head(
|
def head(
|
||||||
@@ -367,9 +316,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **HEAD** *HTTP* method
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
|
|
||||||
@@ -387,8 +334,6 @@ class RouteMixin:
|
|||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests), defaults to True
|
body (eg. GET requests), defaults to True
|
||||||
:type ignore_body: bool, optional
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -399,8 +344,6 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def options(
|
def options(
|
||||||
@@ -411,9 +354,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **OPTIONS** *HTTP* method
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
|
|
||||||
@@ -431,8 +372,6 @@ class RouteMixin:
|
|||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests), defaults to True
|
body (eg. GET requests), defaults to True
|
||||||
:type ignore_body: bool, optional
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -443,8 +382,6 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(
|
def patch(
|
||||||
@@ -455,9 +392,7 @@ class RouteMixin:
|
|||||||
stream=False,
|
stream=False,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PATCH** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
|
|
||||||
@@ -477,8 +412,6 @@ class RouteMixin:
|
|||||||
:param ignore_body: whether the handler should ignore request
|
:param ignore_body: whether the handler should ignore request
|
||||||
body (eg. GET requests), defaults to True
|
body (eg. GET requests), defaults to True
|
||||||
:type ignore_body: bool, optional
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -489,8 +422,6 @@ class RouteMixin:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
@@ -501,9 +432,7 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
):
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> RouteWrapper:
|
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
|
|
||||||
@@ -513,8 +442,6 @@ class RouteMixin:
|
|||||||
URLs need to terminate with a */*
|
URLs need to terminate with a */*
|
||||||
:param version: API Version
|
:param version: API Version
|
||||||
:param name: Unique name that can be used to identify the Route
|
: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: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -525,8 +452,6 @@ class RouteMixin:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def websocket(
|
def websocket(
|
||||||
@@ -538,8 +463,6 @@ class RouteMixin:
|
|||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
version_prefix: str = "/v",
|
|
||||||
error_format: Optional[str] = None,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a websocket route
|
Decorate a function to be registered as a websocket route
|
||||||
@@ -551,8 +474,6 @@ class RouteMixin:
|
|||||||
:param subprotocols: optional list of str with supported subprotocols
|
:param subprotocols: optional list of str with supported subprotocols
|
||||||
:param name: A unique name assigned to the URL so that it can
|
:param name: A unique name assigned to the URL so that it can
|
||||||
be used with :func:`url_for`
|
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: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -565,8 +486,6 @@ class RouteMixin:
|
|||||||
apply=apply,
|
apply=apply,
|
||||||
subprotocols=subprotocols,
|
subprotocols=subprotocols,
|
||||||
websocket=True,
|
websocket=True,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_websocket_route(
|
def add_websocket_route(
|
||||||
@@ -578,8 +497,6 @@ class RouteMixin:
|
|||||||
subprotocols=None,
|
subprotocols=None,
|
||||||
version: Optional[int] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = 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.
|
A helper method to register a function as a websocket route.
|
||||||
@@ -596,8 +513,6 @@ class RouteMixin:
|
|||||||
handshake
|
handshake
|
||||||
:param name: A unique name assigned to the URL so that it can
|
:param name: A unique name assigned to the URL so that it can
|
||||||
be used with :func:`url_for`
|
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: Objected decorated by :func:`websocket`
|
||||||
"""
|
"""
|
||||||
return self.websocket(
|
return self.websocket(
|
||||||
@@ -607,8 +522,6 @@ class RouteMixin:
|
|||||||
subprotocols=subprotocols,
|
subprotocols=subprotocols,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
|
||||||
error_format=error_format,
|
|
||||||
)(handler)
|
)(handler)
|
||||||
|
|
||||||
def static(
|
def static(
|
||||||
@@ -624,7 +537,6 @@ class RouteMixin:
|
|||||||
strict_slashes=None,
|
strict_slashes=None,
|
||||||
content_type=None,
|
content_type=None,
|
||||||
apply=True,
|
apply=True,
|
||||||
resource_type=None,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Register a root to serve files from. The input can either be a
|
Register a root to serve files from. The input can either be a
|
||||||
@@ -674,7 +586,6 @@ class RouteMixin:
|
|||||||
host,
|
host,
|
||||||
strict_slashes,
|
strict_slashes,
|
||||||
content_type,
|
content_type,
|
||||||
resource_type,
|
|
||||||
)
|
)
|
||||||
self._future_statics.add(static)
|
self._future_statics.add(static)
|
||||||
|
|
||||||
@@ -754,10 +665,7 @@ class RouteMixin:
|
|||||||
modified_since = strftime(
|
modified_since = strftime(
|
||||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
||||||
)
|
)
|
||||||
if (
|
if request.headers.get("If-Modified-Since") == modified_since:
|
||||||
request.headers.getone("if-modified-since", None)
|
|
||||||
== modified_since
|
|
||||||
):
|
|
||||||
return HTTPResponse(status=304)
|
return HTTPResponse(status=304)
|
||||||
headers["Last-Modified"] = modified_since
|
headers["Last-Modified"] = modified_since
|
||||||
_range = None
|
_range = None
|
||||||
@@ -810,19 +718,16 @@ class RouteMixin:
|
|||||||
return await file(file_path, headers=headers, _range=_range)
|
return await file(file_path, headers=headers, _range=_range)
|
||||||
except ContentRangeError:
|
except ContentRangeError:
|
||||||
raise
|
raise
|
||||||
except FileNotFoundError:
|
except Exception:
|
||||||
|
error_logger.exception(
|
||||||
|
f"File not found: path={file_or_directory}, "
|
||||||
|
f"relative_url={__file_uri__}"
|
||||||
|
)
|
||||||
raise FileNotFound(
|
raise FileNotFound(
|
||||||
"File not found",
|
"File not found",
|
||||||
path=file_or_directory,
|
path=file_or_directory,
|
||||||
relative_url=__file_uri__,
|
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(
|
def _register_static(
|
||||||
self,
|
self,
|
||||||
@@ -870,27 +775,8 @@ class RouteMixin:
|
|||||||
name = static.name
|
name = static.name
|
||||||
# If we're not trying to match a file directly,
|
# If we're not trying to match a file directly,
|
||||||
# serve from the folder
|
# serve from the folder
|
||||||
if not static.resource_type:
|
if not path.isfile(file_or_directory):
|
||||||
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>"
|
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
|
# special prefix for static files
|
||||||
# if not static.name.startswith("_static_"):
|
# if not static.name.startswith("_static_"):
|
||||||
@@ -907,7 +793,7 @@ class RouteMixin:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
route, _ = self.route( # type: ignore
|
route, _ = self.route(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
methods=["GET", "HEAD"],
|
methods=["GET", "HEAD"],
|
||||||
name=name,
|
name=name,
|
||||||
@@ -917,43 +803,3 @@ class RouteMixin:
|
|||||||
)(_handler)
|
)(_handler)
|
||||||
|
|
||||||
return route
|
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,4 @@
|
|||||||
from typing import Any, Callable, Dict, Optional, Set
|
from typing import Any, Callable, Dict, Set
|
||||||
|
|
||||||
from sanic.models.futures import FutureSignal
|
from sanic.models.futures import FutureSignal
|
||||||
from sanic.models.handler_types import SignalHandler
|
from sanic.models.handler_types import SignalHandler
|
||||||
@@ -23,7 +23,7 @@ class SignalMixin:
|
|||||||
*,
|
*,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
) -> Callable[[SignalHandler], SignalHandler]:
|
) -> Callable[[SignalHandler], FutureSignal]:
|
||||||
"""
|
"""
|
||||||
For creating a signal handler, used similar to a route handler:
|
For creating a signal handler, used similar to a route handler:
|
||||||
|
|
||||||
@@ -54,22 +54,16 @@ class SignalMixin:
|
|||||||
if apply:
|
if apply:
|
||||||
self._apply_signal(future_signal)
|
self._apply_signal(future_signal)
|
||||||
|
|
||||||
return handler
|
return future_signal
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_signal(
|
def add_signal(
|
||||||
self,
|
self,
|
||||||
handler: Optional[Callable[..., Any]],
|
handler,
|
||||||
event: str,
|
event: str,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
):
|
):
|
||||||
if not handler:
|
|
||||||
|
|
||||||
async def noop():
|
|
||||||
...
|
|
||||||
|
|
||||||
handler = noop
|
|
||||||
self.signal(event=event, condition=condition)(handler)
|
self.signal(event=event, condition=condition)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import asyncio
|
|||||||
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
||||||
|
|
||||||
from sanic.exceptions import InvalidUsage
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.server.websockets.connection import WebSocketConnection
|
from sanic.websocket import WebSocketConnection
|
||||||
|
|
||||||
|
|
||||||
ASGIScope = MutableMapping[str, Any]
|
ASGIScope = MutableMapping[str, Any]
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ class FutureRoute(NamedTuple):
|
|||||||
subprotocols: Optional[List[str]]
|
subprotocols: Optional[List[str]]
|
||||||
unquote: bool
|
unquote: bool
|
||||||
static: bool
|
static: bool
|
||||||
version_prefix: str
|
|
||||||
error_format: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class FutureListener(NamedTuple):
|
class FutureListener(NamedTuple):
|
||||||
@@ -53,7 +51,6 @@ class FutureStatic(NamedTuple):
|
|||||||
host: Optional[str]
|
host: Optional[str]
|
||||||
strict_slashes: Optional[bool]
|
strict_slashes: Optional[bool]
|
||||||
content_type: Optional[bool]
|
content_type: Optional[bool]
|
||||||
resource_type: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class FutureSignal(NamedTuple):
|
class FutureSignal(NamedTuple):
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
|
|||||||
ListenerType = Callable[
|
ListenerType = Callable[
|
||||||
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
|
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
|
||||||
]
|
]
|
||||||
RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]]
|
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse]]
|
||||||
SignalHandler = Callable[..., Coroutine[Any, Any, None]]
|
SignalHandler = Callable[..., Coroutine[Any, Any, None]]
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
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",
|
|
||||||
"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.client_ip = ""
|
|
||||||
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_ip = addr[0]
|
|
||||||
self.client_port = addr[1]
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import itertools
|
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -6,9 +5,6 @@ import sys
|
|||||||
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from sanic.config import BASE_LOGO
|
|
||||||
from sanic.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _iter_module_files():
|
def _iter_module_files():
|
||||||
"""This iterates over all relevant Python files.
|
"""This iterates over all relevant Python files.
|
||||||
@@ -60,21 +56,7 @@ def restart_with_reloader():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _check_file(filename, mtimes):
|
def watchdog(sleep_interval):
|
||||||
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.
|
"""Watch project files, restart worker process if a change happened.
|
||||||
|
|
||||||
:param sleep_interval: interval in second.
|
:param sleep_interval: interval in second.
|
||||||
@@ -91,25 +73,21 @@ def watchdog(sleep_interval, app):
|
|||||||
|
|
||||||
worker_process = restart_with_reloader()
|
worker_process = restart_with_reloader()
|
||||||
|
|
||||||
if app.config.LOGO:
|
|
||||||
logger.debug(
|
|
||||||
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
need_reload = False
|
need_reload = False
|
||||||
|
|
||||||
for filename in itertools.chain(
|
for filename in _iter_module_files():
|
||||||
_iter_module_files(),
|
|
||||||
*(d.glob("**/*") for d in app.reload_dirs),
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
check = _check_file(filename, mtimes)
|
mtime = os.stat(filename).st_mtime
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if check:
|
old_time = mtimes.get(filename)
|
||||||
|
if old_time is None:
|
||||||
|
mtimes[filename] = mtime
|
||||||
|
elif mtime > old_time:
|
||||||
|
mtimes[filename] = mtime
|
||||||
need_reload = True
|
need_reload = True
|
||||||
|
|
||||||
if need_reload:
|
if need_reload:
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ from sanic.compat import CancelledErrors, Header
|
|||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||||
from sanic.exceptions import InvalidUsage
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.headers import (
|
from sanic.headers import (
|
||||||
AcceptContainer,
|
|
||||||
Options,
|
Options,
|
||||||
parse_accept,
|
|
||||||
parse_content_header,
|
parse_content_header,
|
||||||
parse_forwarded,
|
parse_forwarded,
|
||||||
parse_host,
|
parse_host,
|
||||||
@@ -96,7 +94,6 @@ class Request:
|
|||||||
"head",
|
"head",
|
||||||
"headers",
|
"headers",
|
||||||
"method",
|
"method",
|
||||||
"parsed_accept",
|
|
||||||
"parsed_args",
|
"parsed_args",
|
||||||
"parsed_not_grouped_args",
|
"parsed_not_grouped_args",
|
||||||
"parsed_files",
|
"parsed_files",
|
||||||
@@ -128,7 +125,7 @@ class Request:
|
|||||||
self._name: Optional[str] = None
|
self._name: Optional[str] = None
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
self.headers = Header(headers)
|
self.headers = headers
|
||||||
self.version = version
|
self.version = version
|
||||||
self.method = method
|
self.method = method
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
@@ -139,7 +136,6 @@ class Request:
|
|||||||
self.conn_info: Optional[ConnInfo] = None
|
self.conn_info: Optional[ConnInfo] = None
|
||||||
self.ctx = SimpleNamespace()
|
self.ctx = SimpleNamespace()
|
||||||
self.parsed_forwarded: Optional[Options] = None
|
self.parsed_forwarded: Optional[Options] = None
|
||||||
self.parsed_accept: Optional[AcceptContainer] = None
|
|
||||||
self.parsed_json = None
|
self.parsed_json = None
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
self.parsed_files = None
|
self.parsed_files = None
|
||||||
@@ -266,7 +262,7 @@ class Request:
|
|||||||
app = Sanic("MyApp", request_class=IntRequest)
|
app = Sanic("MyApp", request_class=IntRequest)
|
||||||
"""
|
"""
|
||||||
if not self._id:
|
if not self._id:
|
||||||
self._id = self.headers.getone(
|
self._id = self.headers.get(
|
||||||
self.app.config.REQUEST_ID_HEADER,
|
self.app.config.REQUEST_ID_HEADER,
|
||||||
self.__class__.generate_id(self), # type: ignore
|
self.__class__.generate_id(self), # type: ignore
|
||||||
)
|
)
|
||||||
@@ -300,13 +296,6 @@ class Request:
|
|||||||
|
|
||||||
return self.parsed_json
|
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
|
@property
|
||||||
def token(self):
|
def token(self):
|
||||||
"""Attempt to return the auth header token.
|
"""Attempt to return the auth header token.
|
||||||
@@ -314,7 +303,7 @@ class Request:
|
|||||||
:return: token related to request
|
:return: token related to request
|
||||||
"""
|
"""
|
||||||
prefixes = ("Bearer", "Token")
|
prefixes = ("Bearer", "Token")
|
||||||
auth_header = self.headers.getone("authorization", None)
|
auth_header = self.headers.get("Authorization")
|
||||||
|
|
||||||
if auth_header is not None:
|
if auth_header is not None:
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
@@ -328,8 +317,8 @@ class Request:
|
|||||||
if self.parsed_form is None:
|
if self.parsed_form is None:
|
||||||
self.parsed_form = RequestParameters()
|
self.parsed_form = RequestParameters()
|
||||||
self.parsed_files = RequestParameters()
|
self.parsed_files = RequestParameters()
|
||||||
content_type = self.headers.getone(
|
content_type = self.headers.get(
|
||||||
"content-type", DEFAULT_HTTP_CONTENT_TYPE
|
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
||||||
)
|
)
|
||||||
content_type, parameters = parse_content_header(content_type)
|
content_type, parameters = parse_content_header(content_type)
|
||||||
try:
|
try:
|
||||||
@@ -389,12 +378,9 @@ class Request:
|
|||||||
:type errors: str
|
:type errors: str
|
||||||
:return: RequestParameters
|
:return: RequestParameters
|
||||||
"""
|
"""
|
||||||
if (
|
if not self.parsed_args[
|
||||||
keep_blank_values,
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
strict_parsing,
|
]:
|
||||||
encoding,
|
|
||||||
errors,
|
|
||||||
) not in self.parsed_args:
|
|
||||||
if self.query_string:
|
if self.query_string:
|
||||||
self.parsed_args[
|
self.parsed_args[
|
||||||
(keep_blank_values, strict_parsing, encoding, errors)
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
@@ -448,12 +434,9 @@ class Request:
|
|||||||
:type errors: str
|
:type errors: str
|
||||||
:return: list
|
:return: list
|
||||||
"""
|
"""
|
||||||
if (
|
if not self.parsed_not_grouped_args[
|
||||||
keep_blank_values,
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
strict_parsing,
|
]:
|
||||||
encoding,
|
|
||||||
errors,
|
|
||||||
) not in self.parsed_not_grouped_args:
|
|
||||||
if self.query_string:
|
if self.query_string:
|
||||||
self.parsed_not_grouped_args[
|
self.parsed_not_grouped_args[
|
||||||
(keep_blank_values, strict_parsing, encoding, errors)
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
@@ -482,7 +465,7 @@ class Request:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
cookie = self.headers.getone("cookie", None)
|
cookie = self.headers.get("Cookie")
|
||||||
if cookie is not None:
|
if cookie is not None:
|
||||||
cookies: SimpleCookie = SimpleCookie()
|
cookies: SimpleCookie = SimpleCookie()
|
||||||
cookies.load(cookie)
|
cookies.load(cookie)
|
||||||
@@ -499,7 +482,7 @@ class Request:
|
|||||||
:return: Content-Type header form the request
|
:return: Content-Type header form the request
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
|
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def match_info(self):
|
def match_info(self):
|
||||||
@@ -508,10 +491,6 @@ class Request:
|
|||||||
"""
|
"""
|
||||||
return self._match_info
|
return self._match_info
|
||||||
|
|
||||||
@match_info.setter
|
|
||||||
def match_info(self, value):
|
|
||||||
self._match_info = value
|
|
||||||
|
|
||||||
# Transport properties (obtained from local interface only)
|
# Transport properties (obtained from local interface only)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -520,7 +499,7 @@ class Request:
|
|||||||
:return: peer ip of the socket
|
:return: peer ip of the socket
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return self.conn_info.client_ip if self.conn_info else ""
|
return self.conn_info.client if self.conn_info else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
@@ -602,7 +581,7 @@ class Request:
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
self.app.websocket_enabled
|
self.app.websocket_enabled
|
||||||
and self.headers.getone("upgrade", "").lower() == "websocket"
|
and self.headers.get("upgrade") == "websocket"
|
||||||
):
|
):
|
||||||
scheme = "ws"
|
scheme = "ws"
|
||||||
else:
|
else:
|
||||||
@@ -629,9 +608,7 @@ class Request:
|
|||||||
server_name = self.app.config.get("SERVER_NAME")
|
server_name = self.app.config.get("SERVER_NAME")
|
||||||
if server_name:
|
if server_name:
|
||||||
return server_name.split("//", 1)[-1].split("/", 1)[0]
|
return server_name.split("//", 1)[-1].split("/", 1)[0]
|
||||||
return str(
|
return str(self.forwarded.get("host") or self.headers.get("host", ""))
|
||||||
self.forwarded.get("host") or self.headers.getone("host", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def server_name(self) -> str:
|
def server_name(self) -> str:
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
**Deprecated** and set for removal in v21.12. You can now achieve the
|
**Deprecated** and set for removal in v21.6. You can now achieve the
|
||||||
same functionality without a callback.
|
same functionality without a callback.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -174,16 +174,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
status: int = 200,
|
status: int = 200,
|
||||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
content_type: str = "text/plain; charset=utf-8",
|
content_type: str = "text/plain; charset=utf-8",
|
||||||
ignore_deprecation_notice: bool = False,
|
chunked="deprecated",
|
||||||
):
|
):
|
||||||
if not ignore_deprecation_notice:
|
if chunked != "deprecated":
|
||||||
warn(
|
warn(
|
||||||
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
|
"The chunked argument has been deprecated and will be "
|
||||||
"will be removed in v21.12. Please upgrade your streaming "
|
"removed in v21.6"
|
||||||
"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__()
|
super().__init__()
|
||||||
@@ -207,9 +203,6 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
self.streaming_fn = None
|
self.streaming_fn = None
|
||||||
await super().send(*args, **kwargs)
|
await super().send(*args, **kwargs)
|
||||||
|
|
||||||
async def eof(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPResponse(BaseHTTPResponse):
|
class HTTPResponse(BaseHTTPResponse):
|
||||||
"""
|
"""
|
||||||
@@ -242,15 +235,6 @@ class HTTPResponse(BaseHTTPResponse):
|
|||||||
self.headers = Header(headers or {})
|
self.headers = Header(headers or {})
|
||||||
self._cookies = None
|
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(
|
def empty(
|
||||||
status=204, headers: Optional[Dict[str, str]] = None
|
status=204, headers: Optional[Dict[str, str]] = None
|
||||||
@@ -412,6 +396,7 @@ async def file_stream(
|
|||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
filename: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
|
chunked="deprecated",
|
||||||
_range: Optional[Range] = None,
|
_range: Optional[Range] = None,
|
||||||
) -> StreamingHTTPResponse:
|
) -> StreamingHTTPResponse:
|
||||||
"""Return a streaming response object with file data.
|
"""Return a streaming response object with file data.
|
||||||
@@ -424,6 +409,12 @@ async def file_stream(
|
|||||||
:param chunked: Deprecated
|
:param chunked: Deprecated
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
|
if chunked != "deprecated":
|
||||||
|
warn(
|
||||||
|
"The chunked argument has been deprecated and will be "
|
||||||
|
"removed in v21.6"
|
||||||
|
)
|
||||||
|
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
if filename:
|
if filename:
|
||||||
headers.setdefault(
|
headers.setdefault(
|
||||||
@@ -462,7 +453,6 @@ async def file_stream(
|
|||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
ignore_deprecation_notice=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -471,6 +461,7 @@ def stream(
|
|||||||
status: int = 200,
|
status: int = 200,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
content_type: str = "text/plain; charset=utf-8",
|
content_type: str = "text/plain; charset=utf-8",
|
||||||
|
chunked="deprecated",
|
||||||
):
|
):
|
||||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||||
@@ -491,12 +482,17 @@ def stream(
|
|||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param chunked: Deprecated
|
:param chunked: Deprecated
|
||||||
"""
|
"""
|
||||||
|
if chunked != "deprecated":
|
||||||
|
warn(
|
||||||
|
"The chunked argument has been deprecated and will be "
|
||||||
|
"removed in v21.6"
|
||||||
|
)
|
||||||
|
|
||||||
return StreamingHTTPResponse(
|
return StreamingHTTPResponse(
|
||||||
streaming_fn,
|
streaming_fn,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
status=status,
|
status=status,
|
||||||
ignore_deprecation_notice=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from inspect import signature
|
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from sanic_routing import BaseRouter # type: ignore
|
from sanic_routing import BaseRouter # type: ignore
|
||||||
from sanic_routing.exceptions import NoMethod # type: ignore
|
from sanic_routing.exceptions import NoMethod # type: ignore
|
||||||
@@ -13,7 +9,6 @@ from sanic_routing.exceptions import (
|
|||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.errorpages import check_error_format
|
|
||||||
from sanic.exceptions import MethodNotSupported, NotFound, SanicException
|
from sanic.exceptions import MethodNotSupported, NotFound, SanicException
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
|
|
||||||
@@ -78,8 +73,6 @@ class Router(BaseRouter):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
unquote: bool = False,
|
unquote: bool = False,
|
||||||
static: bool = False,
|
static: bool = False,
|
||||||
version_prefix: str = "/v",
|
|
||||||
error_format: Optional[str] = None,
|
|
||||||
) -> Union[Route, List[Route]]:
|
) -> Union[Route, List[Route]]:
|
||||||
"""
|
"""
|
||||||
Add a handler to the router
|
Add a handler to the router
|
||||||
@@ -110,14 +103,12 @@ class Router(BaseRouter):
|
|||||||
"""
|
"""
|
||||||
if version is not None:
|
if version is not None:
|
||||||
version = str(version).strip("/").lstrip("v")
|
version = str(version).strip("/").lstrip("v")
|
||||||
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
|
uri = "/".join([f"/v{version}", uri.lstrip("/")])
|
||||||
|
|
||||||
uri = self._normalize(uri, handler)
|
|
||||||
|
|
||||||
params = dict(
|
params = dict(
|
||||||
path=uri,
|
path=uri,
|
||||||
handler=handler,
|
handler=handler,
|
||||||
methods=frozenset(map(str, methods)) if methods else None,
|
methods=methods,
|
||||||
name=name,
|
name=name,
|
||||||
strict=strict_slashes,
|
strict=strict_slashes,
|
||||||
unquote=unquote,
|
unquote=unquote,
|
||||||
@@ -139,10 +130,6 @@ class Router(BaseRouter):
|
|||||||
route.ctx.stream = stream
|
route.ctx.stream = stream
|
||||||
route.ctx.hosts = hosts
|
route.ctx.hosts = hosts
|
||||||
route.ctx.static = static
|
route.ctx.static = static
|
||||||
route.ctx.error_format = error_format
|
|
||||||
|
|
||||||
if error_format:
|
|
||||||
check_error_format(route.ctx.error_format)
|
|
||||||
|
|
||||||
routes.append(route)
|
routes.append(route)
|
||||||
|
|
||||||
@@ -199,24 +186,3 @@ class Router(BaseRouter):
|
|||||||
raise SanicException(
|
raise SanicException(
|
||||||
f"Invalid route: {route}. Parameter names cannot use '__'."
|
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
Normal file
794
sanic/server.py
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
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)
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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",
|
|
||||||
)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from sanic.exceptions import SanicException
|
|
||||||
|
|
||||||
|
|
||||||
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", "init")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
app,
|
|
||||||
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
|
|
||||||
self.init = False
|
|
||||||
|
|
||||||
def startup(self):
|
|
||||||
"""
|
|
||||||
Trigger "before_server_start" events
|
|
||||||
"""
|
|
||||||
self.init = True
|
|
||||||
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):
|
|
||||||
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 _server_event(self, concern: str, action: str):
|
|
||||||
if not self.init:
|
|
||||||
raise SanicException(
|
|
||||||
"Cannot dispatch server event without "
|
|
||||||
"first running 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
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from inspect import isawaitable
|
|
||||||
from typing import Any, Callable, Iterable, Optional
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic.app import Sanic
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from asyncio import CancelledError
|
|
||||||
from asyncio.transports import Transport
|
|
||||||
from time import monotonic as current_time
|
|
||||||
|
|
||||||
from sanic.log import error_logger
|
|
||||||
from sanic.models.server_types import ConnInfo, Signal
|
|
||||||
|
|
||||||
|
|
||||||
class SanicProtocol(asyncio.Protocol):
|
|
||||||
__slots__ = (
|
|
||||||
"app",
|
|
||||||
# event loop, connection
|
|
||||||
"loop",
|
|
||||||
"transport",
|
|
||||||
"connections",
|
|
||||||
"conn_info",
|
|
||||||
"signal",
|
|
||||||
"_can_write",
|
|
||||||
"_time",
|
|
||||||
"_task",
|
|
||||||
"_unix",
|
|
||||||
"_data_received",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
loop,
|
|
||||||
app: Sanic,
|
|
||||||
signal=None,
|
|
||||||
connections=None,
|
|
||||||
unix=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
self.loop = loop
|
|
||||||
self.app: Sanic = app
|
|
||||||
self.signal = signal or Signal()
|
|
||||||
self.transport: Optional[Transport] = None
|
|
||||||
self.connections = connections if connections is not None else set()
|
|
||||||
self.conn_info: Optional[ConnInfo] = None
|
|
||||||
self._can_write = asyncio.Event()
|
|
||||||
self._can_write.set()
|
|
||||||
self._unix = unix
|
|
||||||
self._time = 0.0 # type: float
|
|
||||||
self._task = None # type: Optional[asyncio.Task]
|
|
||||||
self._data_received = asyncio.Event()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ctx(self):
|
|
||||||
if self.conn_info is not None:
|
|
||||||
return self.conn_info.ctx
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def send(self, data):
|
|
||||||
"""
|
|
||||||
Generic data write implementation with backpressure control.
|
|
||||||
"""
|
|
||||||
await self._can_write.wait()
|
|
||||||
if self.transport.is_closing():
|
|
||||||
raise CancelledError
|
|
||||||
self.transport.write(data)
|
|
||||||
self._time = current_time()
|
|
||||||
|
|
||||||
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 close(self, timeout: Optional[float] = None):
|
|
||||||
"""
|
|
||||||
Attempt close the connection.
|
|
||||||
"""
|
|
||||||
# Cause a call to connection_lost where further cleanup occurs
|
|
||||||
if self.transport:
|
|
||||||
self.transport.close()
|
|
||||||
if timeout is None:
|
|
||||||
timeout = self.app.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
|
||||||
self.loop.call_later(timeout, self.abort)
|
|
||||||
|
|
||||||
def abort(self):
|
|
||||||
"""
|
|
||||||
Force close the connection.
|
|
||||||
"""
|
|
||||||
# Cause a call to connection_lost where further cleanup occurs
|
|
||||||
if self.transport:
|
|
||||||
self.transport.abort()
|
|
||||||
self.transport = None
|
|
||||||
|
|
||||||
# asyncio.Protocol API Callbacks #
|
|
||||||
# ------------------------------ #
|
|
||||||
def connection_made(self, transport):
|
|
||||||
"""
|
|
||||||
Generic connection-made, with no connection_task, and no recv_buffer.
|
|
||||||
Override this for protocol-specific connection implementations.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
transport.set_write_buffer_limits(low=16384, high=65536)
|
|
||||||
self.connections.add(self)
|
|
||||||
self.transport = transport
|
|
||||||
self.conn_info = ConnInfo(self.transport, unix=self._unix)
|
|
||||||
except Exception:
|
|
||||||
error_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 BaseException:
|
|
||||||
error_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()
|
|
||||||
|
|
||||||
if self._data_received:
|
|
||||||
self._data_received.set()
|
|
||||||
except BaseException:
|
|
||||||
error_logger.exception("protocol.data_received")
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
|
||||||
|
|
||||||
from sanic.touchup.meta import TouchUpMeta
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic.app import Sanic
|
|
||||||
|
|
||||||
from asyncio import CancelledError
|
|
||||||
from time import monotonic as current_time
|
|
||||||
|
|
||||||
from sanic.exceptions import RequestTimeout, ServiceUnavailable
|
|
||||||
from sanic.http import Http, Stage
|
|
||||||
from sanic.log import error_logger, logger
|
|
||||||
from sanic.models.server_types import ConnInfo
|
|
||||||
from sanic.request import Request
|
|
||||||
from sanic.server.protocols.base_protocol import SanicProtocol
|
|
||||||
|
|
||||||
|
|
||||||
class HttpProtocol(SanicProtocol, metaclass=TouchUpMeta):
|
|
||||||
"""
|
|
||||||
This class provides implements the HTTP 1.1 protocol on top of our
|
|
||||||
Sanic Server transport
|
|
||||||
"""
|
|
||||||
|
|
||||||
__touchup__ = (
|
|
||||||
"send",
|
|
||||||
"connection_task",
|
|
||||||
)
|
|
||||||
__slots__ = (
|
|
||||||
# request params
|
|
||||||
"request",
|
|
||||||
# request config
|
|
||||||
"request_handler",
|
|
||||||
"request_timeout",
|
|
||||||
"response_timeout",
|
|
||||||
"keep_alive_timeout",
|
|
||||||
"request_max_size",
|
|
||||||
"request_class",
|
|
||||||
"error_handler",
|
|
||||||
# enable or disable access log purpose
|
|
||||||
"access_log",
|
|
||||||
# connection management
|
|
||||||
"state",
|
|
||||||
"url",
|
|
||||||
"_handler_task",
|
|
||||||
"_http",
|
|
||||||
"_exception",
|
|
||||||
"recv_buffer",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
loop,
|
|
||||||
app: Sanic,
|
|
||||||
signal=None,
|
|
||||||
connections=None,
|
|
||||||
state=None,
|
|
||||||
unix=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
super().__init__(
|
|
||||||
loop=loop,
|
|
||||||
app=app,
|
|
||||||
signal=signal,
|
|
||||||
connections=connections,
|
|
||||||
unix=unix,
|
|
||||||
)
|
|
||||||
self.url = None
|
|
||||||
self.request: Optional[Request] = None
|
|
||||||
self.access_log = self.app.config.ACCESS_LOG
|
|
||||||
self.request_handler = self.app.handle_request
|
|
||||||
self.error_handler = self.app.error_handler
|
|
||||||
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
|
||||||
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._exception = None
|
|
||||||
|
|
||||||
def _setup_connection(self):
|
|
||||||
self._http = Http(self)
|
|
||||||
self._time = current_time()
|
|
||||||
self.check_timeouts()
|
|
||||||
|
|
||||||
async def connection_task(self): # no cov
|
|
||||||
"""
|
|
||||||
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.app.dispatch(
|
|
||||||
"http.lifecycle.begin",
|
|
||||||
inline=True,
|
|
||||||
context={"conn_info": self.conn_info},
|
|
||||||
)
|
|
||||||
await self._http.http1()
|
|
||||||
except CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
error_logger.exception("protocol.connection_task uncaught")
|
|
||||||
finally:
|
|
||||||
if (
|
|
||||||
self.app.debug
|
|
||||||
and self._http
|
|
||||||
and self.transport
|
|
||||||
and not self._http.upgrade_websocket
|
|
||||||
):
|
|
||||||
ip = self.transport.get_extra_info("peername")
|
|
||||||
error_logger.error(
|
|
||||||
"Connection lost before response written"
|
|
||||||
f" @ {ip} {self._http.request}"
|
|
||||||
)
|
|
||||||
self._http = None
|
|
||||||
self._task = None
|
|
||||||
try:
|
|
||||||
self.close()
|
|
||||||
except BaseException:
|
|
||||||
error_logger.exception("Closing failed")
|
|
||||||
finally:
|
|
||||||
await self.app.dispatch(
|
|
||||||
"http.lifecycle.complete",
|
|
||||||
inline=True,
|
|
||||||
context={"conn_info": self.conn_info},
|
|
||||||
)
|
|
||||||
# Important to keep this Ellipsis here for the TouchUp module
|
|
||||||
...
|
|
||||||
|
|
||||||
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:
|
|
||||||
error_logger.exception("protocol.check_timeouts")
|
|
||||||
|
|
||||||
async def send(self, data): # no cov
|
|
||||||
"""
|
|
||||||
Writes HTTP data with backpressure control.
|
|
||||||
"""
|
|
||||||
await self._can_write.wait()
|
|
||||||
if self.transport.is_closing():
|
|
||||||
raise CancelledError
|
|
||||||
await self.app.dispatch(
|
|
||||||
"http.lifecycle.send",
|
|
||||||
inline=True,
|
|
||||||
context={"data": data},
|
|
||||||
)
|
|
||||||
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
|
|
||||||
|
|
||||||
# -------------------------------------------- #
|
|
||||||
# Only asyncio.Protocol callbacks below this
|
|
||||||
# -------------------------------------------- #
|
|
||||||
|
|
||||||
def connection_made(self, transport):
|
|
||||||
"""
|
|
||||||
HTTP-protocol-specific new connection handler
|
|
||||||
"""
|
|
||||||
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:
|
|
||||||
error_logger.exception("protocol.connect_made")
|
|
||||||
|
|
||||||
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:
|
|
||||||
error_logger.exception("protocol.data_received")
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING, Optional, Sequence
|
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN
|
|
||||||
from websockets.server import ServerConnection
|
|
||||||
|
|
||||||
from sanic.exceptions import ServerError
|
|
||||||
from sanic.log import error_logger
|
|
||||||
from sanic.server import HttpProtocol
|
|
||||||
|
|
||||||
from ..websockets.impl import WebsocketImplProtocol
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from websockets import http11
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketProtocol(HttpProtocol):
|
|
||||||
|
|
||||||
websocket: Optional[WebsocketImplProtocol]
|
|
||||||
websocket_timeout: float
|
|
||||||
websocket_max_size = Optional[int]
|
|
||||||
websocket_ping_interval = Optional[float]
|
|
||||||
websocket_ping_timeout = Optional[float]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*args,
|
|
||||||
websocket_timeout: float = 10.0,
|
|
||||||
websocket_max_size: Optional[int] = None,
|
|
||||||
websocket_max_queue: Optional[int] = None, # max_queue is deprecated
|
|
||||||
websocket_read_limit: Optional[int] = None, # read_limit is deprecated
|
|
||||||
websocket_write_limit: Optional[int] = None, # write_limit deprecated
|
|
||||||
websocket_ping_interval: Optional[float] = 20.0,
|
|
||||||
websocket_ping_timeout: Optional[float] = 20.0,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.websocket = None
|
|
||||||
self.websocket_timeout = websocket_timeout
|
|
||||||
self.websocket_max_size = websocket_max_size
|
|
||||||
if websocket_max_queue is not None and websocket_max_queue > 0:
|
|
||||||
# TODO: Reminder remove this warning in v22.3
|
|
||||||
error_logger.warning(
|
|
||||||
DeprecationWarning(
|
|
||||||
"Websocket no longer uses queueing, so websocket_max_queue"
|
|
||||||
" is no longer required."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if websocket_read_limit is not None and websocket_read_limit > 0:
|
|
||||||
# TODO: Reminder remove this warning in v22.3
|
|
||||||
error_logger.warning(
|
|
||||||
DeprecationWarning(
|
|
||||||
"Websocket no longer uses read buffers, so "
|
|
||||||
"websocket_read_limit is not required."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if websocket_write_limit is not None and websocket_write_limit > 0:
|
|
||||||
# TODO: Reminder remove this warning in v22.3
|
|
||||||
error_logger.warning(
|
|
||||||
DeprecationWarning(
|
|
||||||
"Websocket no longer uses write buffers, so "
|
|
||||||
"websocket_write_limit is not required."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.websocket_ping_interval = websocket_ping_interval
|
|
||||||
self.websocket_ping_timeout = websocket_ping_timeout
|
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
|
||||||
if self.websocket is not None:
|
|
||||||
self.websocket.connection_lost(exc)
|
|
||||||
super().connection_lost(exc)
|
|
||||||
|
|
||||||
def data_received(self, data):
|
|
||||||
if self.websocket is not None:
|
|
||||||
self.websocket.data_received(data)
|
|
||||||
else:
|
|
||||||
# Pass it to HttpProtocol handler first
|
|
||||||
# That will (hopefully) upgrade it to a websocket.
|
|
||||||
super().data_received(data)
|
|
||||||
|
|
||||||
def eof_received(self) -> Optional[bool]:
|
|
||||||
if self.websocket is not None:
|
|
||||||
return self.websocket.eof_received()
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def close(self, timeout: Optional[float] = None):
|
|
||||||
# Called by HttpProtocol at the end of connection_task
|
|
||||||
# If we've upgraded to websocket, we do our own closing
|
|
||||||
if self.websocket is not None:
|
|
||||||
# Note, we don't want to use websocket.close()
|
|
||||||
# That is used for user's application code to send a
|
|
||||||
# websocket close packet. This is different.
|
|
||||||
self.websocket.end_connection(1001)
|
|
||||||
else:
|
|
||||||
super().close()
|
|
||||||
|
|
||||||
def close_if_idle(self):
|
|
||||||
# Called by Sanic Server when shutting down
|
|
||||||
# If we've upgraded to websocket, shut it down
|
|
||||||
if self.websocket is not None:
|
|
||||||
if self.websocket.connection.state in (CLOSING, CLOSED):
|
|
||||||
return True
|
|
||||||
elif self.websocket.loop is not None:
|
|
||||||
self.websocket.loop.create_task(self.websocket.close(1001))
|
|
||||||
else:
|
|
||||||
self.websocket.end_connection(1001)
|
|
||||||
else:
|
|
||||||
return super().close_if_idle()
|
|
||||||
|
|
||||||
async def websocket_handshake(
|
|
||||||
self, request, subprotocols=Optional[Sequence[str]]
|
|
||||||
):
|
|
||||||
# let the websockets package do the handshake with the client
|
|
||||||
try:
|
|
||||||
if subprotocols is not None:
|
|
||||||
# subprotocols can be a set or frozenset,
|
|
||||||
# but ServerConnection needs a list
|
|
||||||
subprotocols = list(subprotocols)
|
|
||||||
ws_conn = ServerConnection(
|
|
||||||
max_size=self.websocket_max_size,
|
|
||||||
subprotocols=subprotocols,
|
|
||||||
state=OPEN,
|
|
||||||
logger=error_logger,
|
|
||||||
)
|
|
||||||
resp: "http11.Response" = ws_conn.accept(request)
|
|
||||||
except Exception:
|
|
||||||
msg = (
|
|
||||||
"Failed to open a WebSocket connection.\n"
|
|
||||||
"See server log for more information.\n"
|
|
||||||
)
|
|
||||||
raise ServerError(msg, status_code=500)
|
|
||||||
if 100 <= resp.status_code <= 299:
|
|
||||||
rbody = "".join(
|
|
||||||
[
|
|
||||||
"HTTP/1.1 ",
|
|
||||||
str(resp.status_code),
|
|
||||||
" ",
|
|
||||||
resp.reason_phrase,
|
|
||||||
"\r\n",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
rbody += "".join(f"{k}: {v}\r\n" for k, v in resp.headers.items())
|
|
||||||
if resp.body is not None:
|
|
||||||
rbody += f"\r\n{resp.body}\r\n\r\n"
|
|
||||||
else:
|
|
||||||
rbody += "\r\n"
|
|
||||||
await super().send(rbody.encode())
|
|
||||||
else:
|
|
||||||
raise ServerError(resp.body, resp.status_code)
|
|
||||||
self.websocket = WebsocketImplProtocol(
|
|
||||||
ws_conn,
|
|
||||||
ping_interval=self.websocket_ping_interval,
|
|
||||||
ping_timeout=self.websocket_ping_timeout,
|
|
||||||
close_timeout=self.websocket_timeout,
|
|
||||||
)
|
|
||||||
loop = (
|
|
||||||
request.transport.loop
|
|
||||||
if hasattr(request, "transport")
|
|
||||||
and hasattr(request.transport, "loop")
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
await self.websocket.connection_made(self, loop=loop)
|
|
||||||
return self.websocket
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ssl import SSLContext
|
|
||||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
|
||||||
|
|
||||||
from sanic.config import Config
|
|
||||||
from sanic.server.events import trigger_events
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic.app import Sanic
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import multiprocessing
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
|
||||||
from signal import signal as signal_func
|
|
||||||
|
|
||||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
|
||||||
from sanic.log import error_logger, logger
|
|
||||||
from sanic.models.server_types import Signal
|
|
||||||
from sanic.server.async_server import AsyncioServer
|
|
||||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
|
||||||
from sanic.server.socket import (
|
|
||||||
bind_socket,
|
|
||||||
bind_unix_socket,
|
|
||||||
remove_unix_socket,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def serve(
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
app: Sanic,
|
|
||||||
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(
|
|
||||||
app=app,
|
|
||||||
loop=loop,
|
|
||||||
serve_coro=server_coroutine,
|
|
||||||
connections=connections,
|
|
||||||
)
|
|
||||||
|
|
||||||
loop.run_until_complete(app._startup())
|
|
||||||
loop.run_until_complete(app._server_event("init", "before"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
|
||||||
except BaseException:
|
|
||||||
error_logger.exception("Unable to start server")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
loop.run_until_complete(app._server_event("init", "after"))
|
|
||||||
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
|
|
||||||
loop.run_until_complete(app._server_event("shutdown", "before"))
|
|
||||||
|
|
||||||
# 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
|
|
||||||
for conn in connections:
|
|
||||||
if hasattr(conn, "websocket") and conn.websocket:
|
|
||||||
conn.websocket.fail_connection(code=1001)
|
|
||||||
else:
|
|
||||||
conn.abort()
|
|
||||||
loop.run_until_complete(app._server_event("shutdown", "after"))
|
|
||||||
|
|
||||||
remove_unix_socket(unix)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
|
|
||||||
"websocket_ping_interval": config.WEBSOCKET_PING_INTERVAL,
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import secrets
|
|
||||||
import socket
|
|
||||||
import stat
|
|
||||||
|
|
||||||
from ipaddress import ip_address
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Awaitable,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
MutableMapping,
|
|
||||||
Optional,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ASIMessage = MutableMapping[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketConnection:
|
|
||||||
"""
|
|
||||||
This is for ASGI Connections.
|
|
||||||
It provides an interface similar to WebsocketProtocol, but
|
|
||||||
sends/receives over an ASGI connection.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
# - Implement ping/pong
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
send: Callable[[ASIMessage], Awaitable[None]],
|
|
||||||
receive: Callable[[], Awaitable[ASIMessage]],
|
|
||||||
subprotocols: Optional[List[str]] = None,
|
|
||||||
) -> None:
|
|
||||||
self._send = send
|
|
||||||
self._receive = receive
|
|
||||||
self._subprotocols = subprotocols or []
|
|
||||||
|
|
||||||
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
|
|
||||||
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
|
|
||||||
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
message.update({"bytes": data})
|
|
||||||
else:
|
|
||||||
message.update({"text": str(data)})
|
|
||||||
|
|
||||||
await self._send(message)
|
|
||||||
|
|
||||||
async def recv(self, *args, **kwargs) -> Optional[str]:
|
|
||||||
message = await self._receive()
|
|
||||||
|
|
||||||
if message["type"] == "websocket.receive":
|
|
||||||
return message["text"]
|
|
||||||
elif message["type"] == "websocket.disconnect":
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
receive = recv
|
|
||||||
|
|
||||||
async def accept(self, subprotocols: Optional[List[str]] = None) -> None:
|
|
||||||
subprotocol = None
|
|
||||||
if subprotocols:
|
|
||||||
for subp in subprotocols:
|
|
||||||
if subp in self.subprotocols:
|
|
||||||
subprotocol = subp
|
|
||||||
break
|
|
||||||
|
|
||||||
await self._send(
|
|
||||||
{
|
|
||||||
"type": "websocket.accept",
|
|
||||||
"subprotocol": subprotocol,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def close(self, code: int = 1000, reason: str = "") -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def subprotocols(self):
|
|
||||||
return self._subprotocols
|
|
||||||
|
|
||||||
@subprotocols.setter
|
|
||||||
def subprotocols(self, subprotocols: Optional[List[str]] = None):
|
|
||||||
self._subprotocols = subprotocols or []
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, AsyncIterator, List, Optional
|
|
||||||
|
|
||||||
from websockets.frames import Frame, Opcode
|
|
||||||
from websockets.typing import Data
|
|
||||||
|
|
||||||
from sanic.exceptions import ServerError
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .impl import WebsocketImplProtocol
|
|
||||||
|
|
||||||
UTF8Decoder = codecs.getincrementaldecoder("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
class WebsocketFrameAssembler:
|
|
||||||
"""
|
|
||||||
Assemble a message from frames.
|
|
||||||
Code borrowed from aaugustin/websockets project:
|
|
||||||
https://github.com/aaugustin/websockets/blob/6eb98dd8fa5b2c896b9f6be7e8d117708da82a39/src/websockets/sync/messages.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"protocol",
|
|
||||||
"read_mutex",
|
|
||||||
"write_mutex",
|
|
||||||
"message_complete",
|
|
||||||
"message_fetched",
|
|
||||||
"get_in_progress",
|
|
||||||
"decoder",
|
|
||||||
"completed_queue",
|
|
||||||
"chunks",
|
|
||||||
"chunks_queue",
|
|
||||||
"paused",
|
|
||||||
"get_id",
|
|
||||||
"put_id",
|
|
||||||
)
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
protocol: "WebsocketImplProtocol"
|
|
||||||
read_mutex: asyncio.Lock
|
|
||||||
write_mutex: asyncio.Lock
|
|
||||||
message_complete: asyncio.Event
|
|
||||||
message_fetched: asyncio.Event
|
|
||||||
completed_queue: asyncio.Queue
|
|
||||||
get_in_progress: bool
|
|
||||||
decoder: Optional[codecs.IncrementalDecoder]
|
|
||||||
# For streaming chunks rather than messages:
|
|
||||||
chunks: List[Data]
|
|
||||||
chunks_queue: Optional[asyncio.Queue[Optional[Data]]]
|
|
||||||
paused: bool
|
|
||||||
|
|
||||||
def __init__(self, protocol) -> None:
|
|
||||||
|
|
||||||
self.protocol = protocol
|
|
||||||
|
|
||||||
self.read_mutex = asyncio.Lock()
|
|
||||||
self.write_mutex = asyncio.Lock()
|
|
||||||
|
|
||||||
self.completed_queue = asyncio.Queue(
|
|
||||||
maxsize=1
|
|
||||||
) # type: asyncio.Queue[Data]
|
|
||||||
|
|
||||||
# put() sets this event to tell get() that a message can be fetched.
|
|
||||||
self.message_complete = asyncio.Event()
|
|
||||||
# get() sets this event to let put()
|
|
||||||
self.message_fetched = asyncio.Event()
|
|
||||||
|
|
||||||
# This flag prevents concurrent calls to get() by user code.
|
|
||||||
self.get_in_progress = False
|
|
||||||
|
|
||||||
# Decoder for text frames, None for binary frames.
|
|
||||||
self.decoder = None
|
|
||||||
|
|
||||||
# Buffer data from frames belonging to the same message.
|
|
||||||
self.chunks = []
|
|
||||||
|
|
||||||
# When switching from "buffering" to "streaming", we use a thread-safe
|
|
||||||
# queue for transferring frames from the writing thread (library code)
|
|
||||||
# to the reading thread (user code). We're buffering when chunks_queue
|
|
||||||
# is None and streaming when it's a Queue. None is a sentinel
|
|
||||||
# value marking the end of the stream, superseding message_complete.
|
|
||||||
|
|
||||||
# Stream data from frames belonging to the same message.
|
|
||||||
self.chunks_queue = None
|
|
||||||
|
|
||||||
# Flag to indicate we've paused the protocol
|
|
||||||
self.paused = False
|
|
||||||
|
|
||||||
async def get(self, timeout: Optional[float] = None) -> Optional[Data]:
|
|
||||||
"""
|
|
||||||
Read the next message.
|
|
||||||
:meth:`get` returns a single :class:`str` or :class:`bytes`.
|
|
||||||
If the :message was fragmented, :meth:`get` waits until the last frame
|
|
||||||
is received, then it reassembles the message.
|
|
||||||
If ``timeout`` is set and elapses before a complete message is
|
|
||||||
received, :meth:`get` returns ``None``.
|
|
||||||
"""
|
|
||||||
async with self.read_mutex:
|
|
||||||
if timeout is not None and timeout <= 0:
|
|
||||||
if not self.message_complete.is_set():
|
|
||||||
return None
|
|
||||||
if self.get_in_progress:
|
|
||||||
# This should be guarded against with the read_mutex,
|
|
||||||
# exception is only here as a failsafe
|
|
||||||
raise ServerError(
|
|
||||||
"Called get() on Websocket frame assembler "
|
|
||||||
"while asynchronous get is already in progress."
|
|
||||||
)
|
|
||||||
self.get_in_progress = True
|
|
||||||
|
|
||||||
# If the message_complete event isn't set yet, release the lock to
|
|
||||||
# allow put() to run and eventually set it.
|
|
||||||
# Locking with get_in_progress ensures only one task can get here.
|
|
||||||
if timeout is None:
|
|
||||||
completed = await self.message_complete.wait()
|
|
||||||
elif timeout <= 0:
|
|
||||||
completed = self.message_complete.is_set()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(
|
|
||||||
self.message_complete.wait(), timeout=timeout
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
...
|
|
||||||
finally:
|
|
||||||
completed = self.message_complete.is_set()
|
|
||||||
|
|
||||||
# Unpause the transport, if its paused
|
|
||||||
if self.paused:
|
|
||||||
self.protocol.resume_frames()
|
|
||||||
self.paused = False
|
|
||||||
if not self.get_in_progress:
|
|
||||||
# This should be guarded against with the read_mutex,
|
|
||||||
# exception is here as a failsafe
|
|
||||||
raise ServerError(
|
|
||||||
"State of Websocket frame assembler was modified while an "
|
|
||||||
"asynchronous get was in progress."
|
|
||||||
)
|
|
||||||
self.get_in_progress = False
|
|
||||||
|
|
||||||
# Waiting for a complete message timed out.
|
|
||||||
if not completed:
|
|
||||||
return None
|
|
||||||
if not self.message_complete.is_set():
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.message_complete.clear()
|
|
||||||
|
|
||||||
joiner: Data = b"" if self.decoder is None else ""
|
|
||||||
# mypy cannot figure out that chunks have the proper type.
|
|
||||||
message: Data = joiner.join(self.chunks) # type: ignore
|
|
||||||
if self.message_fetched.is_set():
|
|
||||||
# This should be guarded against with the read_mutex,
|
|
||||||
# and get_in_progress check, this exception is here
|
|
||||||
# as a failsafe
|
|
||||||
raise ServerError(
|
|
||||||
"Websocket get() found a message when "
|
|
||||||
"state was already fetched."
|
|
||||||
)
|
|
||||||
self.message_fetched.set()
|
|
||||||
self.chunks = []
|
|
||||||
# this should already be None, but set it here for safety
|
|
||||||
self.chunks_queue = None
|
|
||||||
return message
|
|
||||||
|
|
||||||
async def get_iter(self) -> AsyncIterator[Data]:
|
|
||||||
"""
|
|
||||||
Stream the next message.
|
|
||||||
Iterating the return value of :meth:`get_iter` yields a :class:`str`
|
|
||||||
or :class:`bytes` for each frame in the message.
|
|
||||||
"""
|
|
||||||
async with self.read_mutex:
|
|
||||||
if self.get_in_progress:
|
|
||||||
# This should be guarded against with the read_mutex,
|
|
||||||
# exception is only here as a failsafe
|
|
||||||
raise ServerError(
|
|
||||||
"Called get_iter on Websocket frame assembler "
|
|
||||||
"while asynchronous get is already in progress."
|
|
||||||
)
|
|
||||||
self.get_in_progress = True
|
|
||||||
|
|
||||||
chunks = self.chunks
|
|
||||||
self.chunks = []
|
|
||||||
self.chunks_queue = asyncio.Queue()
|
|
||||||
|
|
||||||
# Sending None in chunk_queue supersedes setting message_complete
|
|
||||||
# when switching to "streaming". If message is already complete
|
|
||||||
# when the switch happens, put() didn't send None, so we have to.
|
|
||||||
if self.message_complete.is_set():
|
|
||||||
await self.chunks_queue.put(None)
|
|
||||||
|
|
||||||
# Locking with get_in_progress ensures only one task can get here
|
|
||||||
for c in chunks:
|
|
||||||
yield c
|
|
||||||
while True:
|
|
||||||
chunk = await self.chunks_queue.get()
|
|
||||||
if chunk is None:
|
|
||||||
break
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
# Unpause the transport, if its paused
|
|
||||||
if self.paused:
|
|
||||||
self.protocol.resume_frames()
|
|
||||||
self.paused = False
|
|
||||||
if not self.get_in_progress:
|
|
||||||
# This should be guarded against with the read_mutex,
|
|
||||||
# exception is here as a failsafe
|
|
||||||
raise ServerError(
|
|
||||||
"State of Websocket frame assembler was modified while an "
|
|
||||||
"asynchronous get was in progress."
|
|
||||||
)
|
|
||||||
self.get_in_progress = False
|
|
||||||
if not self.message_complete.is_set():
|
|
||||||
# This should be guarded against with the read_mutex,
|
|
||||||
# exception is here as a failsafe
|
|
||||||
raise ServerError(
|
|
||||||
"Websocket frame assembler chunks queue ended before "
|
|
||||||
"message was complete."
|
|
||||||
)
|
|
||||||
self.message_complete.clear()
|
|
||||||
if self.message_fetched.is_set():
|
|
||||||
# This should be guarded against with the read_mutex,
|
|
||||||
# and get_in_progress check, this exception is
|
|
||||||
# here as a failsafe
|
|
||||||
raise ServerError(
|
|
||||||
"Websocket get_iter() found a message when state was "
|
|
||||||
"already fetched."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.message_fetched.set()
|
|
||||||
# this should already be empty, but set it here for safety
|
|
||||||
self.chunks = []
|
|
||||||
self.chunks_queue = None
|
|
||||||
|
|
||||||
async def put(self, frame: Frame) -> None:
|
|
||||||
"""
|
|
||||||
Add ``frame`` to the next message.
|
|
||||||
When ``frame`` is the final frame in a message, :meth:`put` waits
|
|
||||||
until the message is fetched, either by calling :meth:`get` or by
|
|
||||||
iterating the return value of :meth:`get_iter`.
|
|
||||||
:meth:`put` assumes that the stream of frames respects the protocol.
|
|
||||||
If it doesn't, the behavior is undefined.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async with self.write_mutex:
|
|
||||||
if frame.opcode is Opcode.TEXT:
|
|
||||||
self.decoder = UTF8Decoder(errors="strict")
|
|
||||||
elif frame.opcode is Opcode.BINARY:
|
|
||||||
self.decoder = None
|
|
||||||
elif frame.opcode is Opcode.CONT:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Ignore control frames.
|
|
||||||
return
|
|
||||||
data: Data
|
|
||||||
if self.decoder is not None:
|
|
||||||
data = self.decoder.decode(frame.data, frame.fin)
|
|
||||||
else:
|
|
||||||
data = frame.data
|
|
||||||
if self.chunks_queue is None:
|
|
||||||
self.chunks.append(data)
|
|
||||||
else:
|
|
||||||
await self.chunks_queue.put(data)
|
|
||||||
|
|
||||||
if not frame.fin:
|
|
||||||
return
|
|
||||||
if not self.get_in_progress:
|
|
||||||
# nobody is waiting for this frame, so try to pause subsequent
|
|
||||||
# frames at the protocol level
|
|
||||||
self.paused = self.protocol.pause_frames()
|
|
||||||
# Message is complete. Wait until it's fetched to return.
|
|
||||||
|
|
||||||
if self.chunks_queue is not None:
|
|
||||||
await self.chunks_queue.put(None)
|
|
||||||
if self.message_complete.is_set():
|
|
||||||
# This should be guarded against with the write_mutex
|
|
||||||
raise ServerError(
|
|
||||||
"Websocket put() got a new message when a message was "
|
|
||||||
"already in its chamber."
|
|
||||||
)
|
|
||||||
self.message_complete.set() # Signal to get() it can serve the
|
|
||||||
if self.message_fetched.is_set():
|
|
||||||
# This should be guarded against with the write_mutex
|
|
||||||
raise ServerError(
|
|
||||||
"Websocket put() got a new message when the previous "
|
|
||||||
"message was not yet fetched."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Allow get() to run and eventually set the event.
|
|
||||||
await self.message_fetched.wait()
|
|
||||||
self.message_fetched.clear()
|
|
||||||
self.decoder = None
|
|
||||||
@@ -1,834 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import random
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
AsyncIterator,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
Mapping,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN, Event
|
|
||||||
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
|
|
||||||
from websockets.frames import Frame, Opcode
|
|
||||||
from websockets.server import ServerConnection
|
|
||||||
from websockets.typing import Data
|
|
||||||
|
|
||||||
from sanic.log import error_logger, logger
|
|
||||||
from sanic.server.protocols.base_protocol import SanicProtocol
|
|
||||||
|
|
||||||
from ...exceptions import ServerError, WebsocketClosed
|
|
||||||
from .frame import WebsocketFrameAssembler
|
|
||||||
|
|
||||||
|
|
||||||
class WebsocketImplProtocol:
|
|
||||||
connection: ServerConnection
|
|
||||||
io_proto: Optional[SanicProtocol]
|
|
||||||
loop: Optional[asyncio.AbstractEventLoop]
|
|
||||||
max_queue: int
|
|
||||||
close_timeout: float
|
|
||||||
ping_interval: Optional[float]
|
|
||||||
ping_timeout: Optional[float]
|
|
||||||
assembler: WebsocketFrameAssembler
|
|
||||||
# Dict[bytes, asyncio.Future[None]]
|
|
||||||
pings: Dict[bytes, asyncio.Future]
|
|
||||||
conn_mutex: asyncio.Lock
|
|
||||||
recv_lock: asyncio.Lock
|
|
||||||
recv_cancel: Optional[asyncio.Future]
|
|
||||||
process_event_mutex: asyncio.Lock
|
|
||||||
can_pause: bool
|
|
||||||
# Optional[asyncio.Future[None]]
|
|
||||||
data_finished_fut: Optional[asyncio.Future]
|
|
||||||
# Optional[asyncio.Future[None]]
|
|
||||||
pause_frame_fut: Optional[asyncio.Future]
|
|
||||||
# Optional[asyncio.Future[None]]
|
|
||||||
connection_lost_waiter: Optional[asyncio.Future]
|
|
||||||
keepalive_ping_task: Optional[asyncio.Task]
|
|
||||||
auto_closer_task: Optional[asyncio.Task]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
connection,
|
|
||||||
max_queue=None,
|
|
||||||
ping_interval: Optional[float] = 20,
|
|
||||||
ping_timeout: Optional[float] = 20,
|
|
||||||
close_timeout: float = 10,
|
|
||||||
loop=None,
|
|
||||||
):
|
|
||||||
self.connection = connection
|
|
||||||
self.io_proto = None
|
|
||||||
self.loop = None
|
|
||||||
self.max_queue = max_queue
|
|
||||||
self.close_timeout = close_timeout
|
|
||||||
self.ping_interval = ping_interval
|
|
||||||
self.ping_timeout = ping_timeout
|
|
||||||
self.assembler = WebsocketFrameAssembler(self)
|
|
||||||
self.pings = {}
|
|
||||||
self.conn_mutex = asyncio.Lock()
|
|
||||||
self.recv_lock = asyncio.Lock()
|
|
||||||
self.recv_cancel = None
|
|
||||||
self.process_event_mutex = asyncio.Lock()
|
|
||||||
self.data_finished_fut = None
|
|
||||||
self.can_pause = True
|
|
||||||
self.pause_frame_fut = None
|
|
||||||
self.keepalive_ping_task = None
|
|
||||||
self.auto_closer_task = None
|
|
||||||
self.connection_lost_waiter = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def subprotocol(self):
|
|
||||||
return self.connection.subprotocol
|
|
||||||
|
|
||||||
def pause_frames(self):
|
|
||||||
if not self.can_pause:
|
|
||||||
return False
|
|
||||||
if self.pause_frame_fut:
|
|
||||||
logger.debug("Websocket connection already paused.")
|
|
||||||
return False
|
|
||||||
if (not self.loop) or (not self.io_proto):
|
|
||||||
return False
|
|
||||||
if self.io_proto.transport:
|
|
||||||
self.io_proto.transport.pause_reading()
|
|
||||||
self.pause_frame_fut = self.loop.create_future()
|
|
||||||
logger.debug("Websocket connection paused.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def resume_frames(self):
|
|
||||||
if not self.pause_frame_fut:
|
|
||||||
logger.debug("Websocket connection not paused.")
|
|
||||||
return False
|
|
||||||
if (not self.loop) or (not self.io_proto):
|
|
||||||
logger.debug(
|
|
||||||
"Websocket attempting to resume reading frames, "
|
|
||||||
"but connection is gone."
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
if self.io_proto.transport:
|
|
||||||
self.io_proto.transport.resume_reading()
|
|
||||||
self.pause_frame_fut.set_result(None)
|
|
||||||
self.pause_frame_fut = None
|
|
||||||
logger.debug("Websocket connection unpaused.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def connection_made(
|
|
||||||
self,
|
|
||||||
io_proto: SanicProtocol,
|
|
||||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
||||||
):
|
|
||||||
if not loop:
|
|
||||||
try:
|
|
||||||
loop = getattr(io_proto, "loop")
|
|
||||||
except AttributeError:
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
if not loop:
|
|
||||||
# This catch is for mypy type checker
|
|
||||||
# to assert loop is not None here.
|
|
||||||
raise ServerError("Connection received with no asyncio loop.")
|
|
||||||
if self.auto_closer_task:
|
|
||||||
raise ServerError(
|
|
||||||
"Cannot call connection_made more than once "
|
|
||||||
"on a websocket connection."
|
|
||||||
)
|
|
||||||
self.loop = loop
|
|
||||||
self.io_proto = io_proto
|
|
||||||
self.connection_lost_waiter = self.loop.create_future()
|
|
||||||
self.data_finished_fut = asyncio.shield(self.loop.create_future())
|
|
||||||
|
|
||||||
if self.ping_interval:
|
|
||||||
self.keepalive_ping_task = asyncio.create_task(
|
|
||||||
self.keepalive_ping()
|
|
||||||
)
|
|
||||||
self.auto_closer_task = asyncio.create_task(
|
|
||||||
self.auto_close_connection()
|
|
||||||
)
|
|
||||||
|
|
||||||
async def wait_for_connection_lost(self, timeout=None) -> bool:
|
|
||||||
"""
|
|
||||||
Wait until the TCP connection is closed or ``timeout`` elapses.
|
|
||||||
If timeout is None, wait forever.
|
|
||||||
Recommend you should pass in self.close_timeout as timeout
|
|
||||||
|
|
||||||
Return ``True`` if the connection is closed and ``False`` otherwise.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not self.connection_lost_waiter:
|
|
||||||
return False
|
|
||||||
if self.connection_lost_waiter.done():
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(
|
|
||||||
asyncio.shield(self.connection_lost_waiter), timeout
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
# Re-check self.connection_lost_waiter.done() synchronously
|
|
||||||
# because connection_lost() could run between the moment the
|
|
||||||
# timeout occurs and the moment this coroutine resumes running
|
|
||||||
return self.connection_lost_waiter.done()
|
|
||||||
|
|
||||||
async def process_events(self, events: Sequence[Event]) -> None:
|
|
||||||
"""
|
|
||||||
Process a list of incoming events.
|
|
||||||
"""
|
|
||||||
# Wrapped in a mutex lock, to prevent other incoming events
|
|
||||||
# from processing at the same time
|
|
||||||
async with self.process_event_mutex:
|
|
||||||
for event in events:
|
|
||||||
if not isinstance(event, Frame):
|
|
||||||
# Event is not a frame. Ignore it.
|
|
||||||
continue
|
|
||||||
if event.opcode == Opcode.PONG:
|
|
||||||
await self.process_pong(event)
|
|
||||||
elif event.opcode == Opcode.CLOSE:
|
|
||||||
if self.recv_cancel:
|
|
||||||
self.recv_cancel.cancel()
|
|
||||||
else:
|
|
||||||
await self.assembler.put(event)
|
|
||||||
|
|
||||||
async def process_pong(self, frame: Frame) -> None:
|
|
||||||
if frame.data in self.pings:
|
|
||||||
# Acknowledge all pings up to the one matching this pong.
|
|
||||||
ping_ids = []
|
|
||||||
for ping_id, ping in self.pings.items():
|
|
||||||
ping_ids.append(ping_id)
|
|
||||||
if not ping.done():
|
|
||||||
ping.set_result(None)
|
|
||||||
if ping_id == frame.data:
|
|
||||||
break
|
|
||||||
else: # noqa
|
|
||||||
raise ServerError("ping_id is not in self.pings")
|
|
||||||
# Remove acknowledged pings from self.pings.
|
|
||||||
for ping_id in ping_ids:
|
|
||||||
del self.pings[ping_id]
|
|
||||||
|
|
||||||
async def keepalive_ping(self) -> None:
|
|
||||||
"""
|
|
||||||
Send a Ping frame and wait for a Pong frame at regular intervals.
|
|
||||||
This coroutine exits when the connection terminates and one of the
|
|
||||||
following happens:
|
|
||||||
- :meth:`ping` raises :exc:`ConnectionClosed`, or
|
|
||||||
- :meth:`auto_close_connection` cancels :attr:`keepalive_ping_task`.
|
|
||||||
"""
|
|
||||||
if self.ping_interval is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(self.ping_interval)
|
|
||||||
|
|
||||||
# ping() raises CancelledError if the connection is closed,
|
|
||||||
# when auto_close_connection() cancels keepalive_ping_task.
|
|
||||||
|
|
||||||
# ping() raises ConnectionClosed if the connection is lost,
|
|
||||||
# when connection_lost() calls abort_pings().
|
|
||||||
|
|
||||||
ping_waiter = await self.ping()
|
|
||||||
|
|
||||||
if self.ping_timeout is not None:
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(ping_waiter, self.ping_timeout)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
error_logger.warning(
|
|
||||||
"Websocket timed out waiting for pong"
|
|
||||||
)
|
|
||||||
self.fail_connection(1011)
|
|
||||||
break
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
# It is expected for this task to be cancelled during during
|
|
||||||
# normal operation, when the connection is closed.
|
|
||||||
logger.debug("Websocket keepalive ping task was cancelled.")
|
|
||||||
except (ConnectionClosed, WebsocketClosed):
|
|
||||||
logger.debug("Websocket closed. Keepalive ping task exiting.")
|
|
||||||
except Exception as e:
|
|
||||||
error_logger.warning(
|
|
||||||
"Unexpected exception in websocket keepalive ping task."
|
|
||||||
)
|
|
||||||
logger.debug(str(e))
|
|
||||||
|
|
||||||
def _force_disconnect(self) -> bool:
|
|
||||||
"""
|
|
||||||
Internal methdod used by end_connection and fail_connection
|
|
||||||
only when the graceful auto-closer cannot be used
|
|
||||||
"""
|
|
||||||
if self.auto_closer_task and not self.auto_closer_task.done():
|
|
||||||
self.auto_closer_task.cancel()
|
|
||||||
if self.data_finished_fut and not self.data_finished_fut.done():
|
|
||||||
self.data_finished_fut.cancel()
|
|
||||||
self.data_finished_fut = None
|
|
||||||
if self.keepalive_ping_task and not self.keepalive_ping_task.done():
|
|
||||||
self.keepalive_ping_task.cancel()
|
|
||||||
self.keepalive_ping_task = None
|
|
||||||
if self.loop and self.io_proto and self.io_proto.transport:
|
|
||||||
self.io_proto.transport.close()
|
|
||||||
self.loop.call_later(
|
|
||||||
self.close_timeout, self.io_proto.transport.abort
|
|
||||||
)
|
|
||||||
# We were never open, or already closed
|
|
||||||
return True
|
|
||||||
|
|
||||||
def fail_connection(self, code: int = 1006, reason: str = "") -> bool:
|
|
||||||
"""
|
|
||||||
Fail the WebSocket Connection
|
|
||||||
This requires:
|
|
||||||
1. Stopping all processing of incoming data, which means cancelling
|
|
||||||
pausing the underlying io protocol. The close code will be 1006
|
|
||||||
unless a close frame was received earlier.
|
|
||||||
2. Sending a close frame with an appropriate code if the opening
|
|
||||||
handshake succeeded and the other side is likely to process it.
|
|
||||||
3. Closing the connection. :meth:`auto_close_connection` takes care
|
|
||||||
of this.
|
|
||||||
(The specification describes these steps in the opposite order.)
|
|
||||||
"""
|
|
||||||
if self.io_proto and self.io_proto.transport:
|
|
||||||
# Stop new data coming in
|
|
||||||
# In Python Version 3.7: pause_reading is idempotent
|
|
||||||
# ut can be called when the transport is already paused or closed
|
|
||||||
self.io_proto.transport.pause_reading()
|
|
||||||
|
|
||||||
# Keeping fail_connection() synchronous guarantees it can't
|
|
||||||
# get stuck and simplifies the implementation of the callers.
|
|
||||||
# Not draining the write buffer is acceptable in this context.
|
|
||||||
|
|
||||||
# clear the send buffer
|
|
||||||
_ = self.connection.data_to_send()
|
|
||||||
# If we're not already CLOSED or CLOSING, then send the close.
|
|
||||||
if self.connection.state is OPEN:
|
|
||||||
if code in (1000, 1001):
|
|
||||||
self.connection.send_close(code, reason)
|
|
||||||
else:
|
|
||||||
self.connection.fail(code, reason)
|
|
||||||
try:
|
|
||||||
data_to_send = self.connection.data_to_send()
|
|
||||||
while (
|
|
||||||
len(data_to_send)
|
|
||||||
and self.io_proto
|
|
||||||
and self.io_proto.transport
|
|
||||||
):
|
|
||||||
frame_data = data_to_send.pop(0)
|
|
||||||
self.io_proto.transport.write(frame_data)
|
|
||||||
except Exception:
|
|
||||||
# sending close frames may fail if the
|
|
||||||
# transport closes during this period
|
|
||||||
...
|
|
||||||
if code == 1006:
|
|
||||||
# Special case: 1006 consider the transport already closed
|
|
||||||
self.connection.state = CLOSED
|
|
||||||
if self.data_finished_fut and not self.data_finished_fut.done():
|
|
||||||
# We have a graceful auto-closer. Use it to close the connection.
|
|
||||||
self.data_finished_fut.cancel()
|
|
||||||
self.data_finished_fut = None
|
|
||||||
if (not self.auto_closer_task) or self.auto_closer_task.done():
|
|
||||||
return self._force_disconnect()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def end_connection(self, code=1000, reason=""):
|
|
||||||
# This is like slightly more graceful form of fail_connection
|
|
||||||
# Use this instead of close() when you need an immediate
|
|
||||||
# close and cannot await websocket.close() handshake.
|
|
||||||
|
|
||||||
if code == 1006 or not self.io_proto or not self.io_proto.transport:
|
|
||||||
return self.fail_connection(code, reason)
|
|
||||||
|
|
||||||
# Stop new data coming in
|
|
||||||
# In Python Version 3.7: pause_reading is idempotent
|
|
||||||
# i.e. it can be called when the transport is already paused or closed.
|
|
||||||
self.io_proto.transport.pause_reading()
|
|
||||||
if self.connection.state == OPEN:
|
|
||||||
data_to_send = self.connection.data_to_send()
|
|
||||||
self.connection.send_close(code, reason)
|
|
||||||
data_to_send.extend(self.connection.data_to_send())
|
|
||||||
try:
|
|
||||||
while (
|
|
||||||
len(data_to_send)
|
|
||||||
and self.io_proto
|
|
||||||
and self.io_proto.transport
|
|
||||||
):
|
|
||||||
frame_data = data_to_send.pop(0)
|
|
||||||
self.io_proto.transport.write(frame_data)
|
|
||||||
except Exception:
|
|
||||||
# sending close frames may fail if the
|
|
||||||
# transport closes during this period
|
|
||||||
# But that doesn't matter at this point
|
|
||||||
...
|
|
||||||
if self.data_finished_fut and not self.data_finished_fut.done():
|
|
||||||
# We have the ability to signal the auto-closer
|
|
||||||
# try to trigger it to auto-close the connection
|
|
||||||
self.data_finished_fut.cancel()
|
|
||||||
self.data_finished_fut = None
|
|
||||||
if (not self.auto_closer_task) or self.auto_closer_task.done():
|
|
||||||
# Auto-closer is not running, do force disconnect
|
|
||||||
return self._force_disconnect()
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def auto_close_connection(self) -> None:
|
|
||||||
"""
|
|
||||||
Close the WebSocket Connection
|
|
||||||
When the opening handshake succeeds, :meth:`connection_open` starts
|
|
||||||
this coroutine in a task. It waits for the data transfer phase to
|
|
||||||
complete then it closes the TCP connection cleanly.
|
|
||||||
When the opening handshake fails, :meth:`fail_connection` does the
|
|
||||||
same. There's no data transfer phase in that case.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Wait for the data transfer phase to complete.
|
|
||||||
if self.data_finished_fut:
|
|
||||||
try:
|
|
||||||
await self.data_finished_fut
|
|
||||||
logger.debug(
|
|
||||||
"Websocket task finished. Closing the connection."
|
|
||||||
)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
# Cancelled error is called when data phase is cancelled
|
|
||||||
# if an error occurred or the client closed the connection
|
|
||||||
logger.debug(
|
|
||||||
"Websocket handler cancelled. Closing the connection."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cancel the keepalive ping task.
|
|
||||||
if self.keepalive_ping_task:
|
|
||||||
self.keepalive_ping_task.cancel()
|
|
||||||
self.keepalive_ping_task = None
|
|
||||||
|
|
||||||
# Half-close the TCP connection if possible (when there's no TLS).
|
|
||||||
if (
|
|
||||||
self.io_proto
|
|
||||||
and self.io_proto.transport
|
|
||||||
and self.io_proto.transport.can_write_eof()
|
|
||||||
):
|
|
||||||
logger.debug("Websocket half-closing TCP connection")
|
|
||||||
self.io_proto.transport.write_eof()
|
|
||||||
if self.connection_lost_waiter:
|
|
||||||
if await self.wait_for_connection_lost(timeout=0):
|
|
||||||
return
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
...
|
|
||||||
finally:
|
|
||||||
# The try/finally ensures that the transport never remains open,
|
|
||||||
# even if this coroutine is cancelled (for example).
|
|
||||||
if (not self.io_proto) or (not self.io_proto.transport):
|
|
||||||
# we were never open, or done. Can't do any finalization.
|
|
||||||
return
|
|
||||||
elif (
|
|
||||||
self.connection_lost_waiter
|
|
||||||
and self.connection_lost_waiter.done()
|
|
||||||
):
|
|
||||||
# connection confirmed closed already, proceed to abort waiter
|
|
||||||
...
|
|
||||||
elif self.io_proto.transport.is_closing():
|
|
||||||
# Connection is already closing (due to half-close above)
|
|
||||||
# proceed to abort waiter
|
|
||||||
...
|
|
||||||
else:
|
|
||||||
self.io_proto.transport.close()
|
|
||||||
if not self.connection_lost_waiter:
|
|
||||||
# Our connection monitor task isn't running.
|
|
||||||
try:
|
|
||||||
await asyncio.sleep(self.close_timeout)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
...
|
|
||||||
if self.io_proto and self.io_proto.transport:
|
|
||||||
self.io_proto.transport.abort()
|
|
||||||
else:
|
|
||||||
if await self.wait_for_connection_lost(
|
|
||||||
timeout=self.close_timeout
|
|
||||||
):
|
|
||||||
# Connection aborted before the timeout expired.
|
|
||||||
return
|
|
||||||
error_logger.warning(
|
|
||||||
"Timeout waiting for TCP connection to close. Aborting"
|
|
||||||
)
|
|
||||||
if self.io_proto and self.io_proto.transport:
|
|
||||||
self.io_proto.transport.abort()
|
|
||||||
|
|
||||||
def abort_pings(self) -> None:
|
|
||||||
"""
|
|
||||||
Raise ConnectionClosed in pending keepalive pings.
|
|
||||||
They'll never receive a pong once the connection is closed.
|
|
||||||
"""
|
|
||||||
if self.connection.state is not CLOSED:
|
|
||||||
raise ServerError(
|
|
||||||
"Webscoket about_pings should only be called "
|
|
||||||
"after connection state is changed to CLOSED"
|
|
||||||
)
|
|
||||||
|
|
||||||
for ping in self.pings.values():
|
|
||||||
ping.set_exception(ConnectionClosedError(None, None))
|
|
||||||
# If the exception is never retrieved, it will be logged when ping
|
|
||||||
# is garbage-collected. This is confusing for users.
|
|
||||||
# Given that ping is done (with an exception), canceling it does
|
|
||||||
# nothing, but it prevents logging the exception.
|
|
||||||
ping.cancel()
|
|
||||||
|
|
||||||
async def close(self, code: int = 1000, reason: str = "") -> None:
|
|
||||||
"""
|
|
||||||
Perform the closing handshake.
|
|
||||||
This is a websocket-protocol level close.
|
|
||||||
:meth:`close` waits for the other end to complete the handshake and
|
|
||||||
for the TCP connection to terminate.
|
|
||||||
:meth:`close` is idempotent: it doesn't do anything once the
|
|
||||||
connection is closed.
|
|
||||||
:param code: WebSocket close code
|
|
||||||
:param reason: WebSocket close reason
|
|
||||||
"""
|
|
||||||
if code == 1006:
|
|
||||||
self.fail_connection(code, reason)
|
|
||||||
return
|
|
||||||
async with self.conn_mutex:
|
|
||||||
if self.connection.state is OPEN:
|
|
||||||
self.connection.send_close(code, reason)
|
|
||||||
data_to_send = self.connection.data_to_send()
|
|
||||||
await self.send_data(data_to_send)
|
|
||||||
|
|
||||||
async def recv(self, timeout: Optional[float] = None) -> Optional[Data]:
|
|
||||||
"""
|
|
||||||
Receive the next message.
|
|
||||||
Return a :class:`str` for a text frame and :class:`bytes` for a binary
|
|
||||||
frame.
|
|
||||||
When the end of the message stream is reached, :meth:`recv` raises
|
|
||||||
:exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it
|
|
||||||
raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
|
|
||||||
connection closure and
|
|
||||||
:exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
|
|
||||||
error or a network failure.
|
|
||||||
If ``timeout`` is ``None``, block until a message is received. Else,
|
|
||||||
if no message is received within ``timeout`` seconds, return ``None``.
|
|
||||||
Set ``timeout`` to ``0`` to check if a message was already received.
|
|
||||||
:raises ~websockets.exceptions.ConnectionClosed: when the
|
|
||||||
connection is closed
|
|
||||||
:raises asyncio.CancelledError: if the websocket closes while waiting
|
|
||||||
:raises ServerError: if two tasks call :meth:`recv` or
|
|
||||||
:meth:`recv_streaming` concurrently
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.recv_lock.locked():
|
|
||||||
raise ServerError(
|
|
||||||
"cannot call recv while another task is "
|
|
||||||
"already waiting for the next message"
|
|
||||||
)
|
|
||||||
await self.recv_lock.acquire()
|
|
||||||
if self.connection.state is CLOSED:
|
|
||||||
self.recv_lock.release()
|
|
||||||
raise WebsocketClosed(
|
|
||||||
"Cannot receive from websocket interface after it is closed."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
self.recv_cancel = asyncio.Future()
|
|
||||||
done, pending = await asyncio.wait(
|
|
||||||
(self.recv_cancel, self.assembler.get(timeout)),
|
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
|
||||||
)
|
|
||||||
done_task = next(iter(done))
|
|
||||||
if done_task is self.recv_cancel:
|
|
||||||
# recv was cancelled
|
|
||||||
for p in pending:
|
|
||||||
p.cancel()
|
|
||||||
raise asyncio.CancelledError()
|
|
||||||
else:
|
|
||||||
self.recv_cancel.cancel()
|
|
||||||
return done_task.result()
|
|
||||||
finally:
|
|
||||||
self.recv_cancel = None
|
|
||||||
self.recv_lock.release()
|
|
||||||
|
|
||||||
async def recv_burst(self, max_recv=256) -> Sequence[Data]:
|
|
||||||
"""
|
|
||||||
Receive the messages which have arrived since last checking.
|
|
||||||
Return a :class:`list` containing :class:`str` for a text frame
|
|
||||||
and :class:`bytes` for a binary frame.
|
|
||||||
When the end of the message stream is reached, :meth:`recv_burst`
|
|
||||||
raises :exc:`~websockets.exceptions.ConnectionClosed`. Specifically,
|
|
||||||
it raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a
|
|
||||||
normal connection closure and
|
|
||||||
:exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
|
|
||||||
error or a network failure.
|
|
||||||
:raises ~websockets.exceptions.ConnectionClosed: when the
|
|
||||||
connection is closed
|
|
||||||
:raises ServerError: if two tasks call :meth:`recv_burst` or
|
|
||||||
:meth:`recv_streaming` concurrently
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.recv_lock.locked():
|
|
||||||
raise ServerError(
|
|
||||||
"cannot call recv_burst while another task is already waiting "
|
|
||||||
"for the next message"
|
|
||||||
)
|
|
||||||
await self.recv_lock.acquire()
|
|
||||||
if self.connection.state is CLOSED:
|
|
||||||
self.recv_lock.release()
|
|
||||||
raise WebsocketClosed(
|
|
||||||
"Cannot receive from websocket interface after it is closed."
|
|
||||||
)
|
|
||||||
messages = []
|
|
||||||
try:
|
|
||||||
# Prevent pausing the transport when we're
|
|
||||||
# receiving a burst of messages
|
|
||||||
self.can_pause = False
|
|
||||||
self.recv_cancel = asyncio.Future()
|
|
||||||
while True:
|
|
||||||
done, pending = await asyncio.wait(
|
|
||||||
(self.recv_cancel, self.assembler.get(timeout=0)),
|
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
|
||||||
)
|
|
||||||
done_task = next(iter(done))
|
|
||||||
if done_task is self.recv_cancel:
|
|
||||||
# recv_burst was cancelled
|
|
||||||
for p in pending:
|
|
||||||
p.cancel()
|
|
||||||
raise asyncio.CancelledError()
|
|
||||||
m = done_task.result()
|
|
||||||
if m is None:
|
|
||||||
# None left in the burst. This is good!
|
|
||||||
break
|
|
||||||
messages.append(m)
|
|
||||||
if len(messages) >= max_recv:
|
|
||||||
# Too much data in the pipe. Hit our burst limit.
|
|
||||||
break
|
|
||||||
# Allow an eventloop iteration for the
|
|
||||||
# next message to pass into the Assembler
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
self.recv_cancel.cancel()
|
|
||||||
finally:
|
|
||||||
self.recv_cancel = None
|
|
||||||
self.can_pause = True
|
|
||||||
self.recv_lock.release()
|
|
||||||
return messages
|
|
||||||
|
|
||||||
async def recv_streaming(self) -> AsyncIterator[Data]:
|
|
||||||
"""
|
|
||||||
Receive the next message frame by frame.
|
|
||||||
Return an iterator of :class:`str` for a text frame and :class:`bytes`
|
|
||||||
for a binary frame. The iterator should be exhausted, or else the
|
|
||||||
connection will become unusable.
|
|
||||||
With the exception of the return value, :meth:`recv_streaming` behaves
|
|
||||||
like :meth:`recv`.
|
|
||||||
"""
|
|
||||||
if self.recv_lock.locked():
|
|
||||||
raise ServerError(
|
|
||||||
"Cannot call recv_streaming while another task "
|
|
||||||
"is already waiting for the next message"
|
|
||||||
)
|
|
||||||
await self.recv_lock.acquire()
|
|
||||||
if self.connection.state is CLOSED:
|
|
||||||
self.recv_lock.release()
|
|
||||||
raise WebsocketClosed(
|
|
||||||
"Cannot receive from websocket interface after it is closed."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
cancelled = False
|
|
||||||
self.recv_cancel = asyncio.Future()
|
|
||||||
self.can_pause = False
|
|
||||||
async for m in self.assembler.get_iter():
|
|
||||||
if self.recv_cancel.done():
|
|
||||||
cancelled = True
|
|
||||||
break
|
|
||||||
yield m
|
|
||||||
if cancelled:
|
|
||||||
raise asyncio.CancelledError()
|
|
||||||
finally:
|
|
||||||
self.can_pause = True
|
|
||||||
self.recv_cancel = None
|
|
||||||
self.recv_lock.release()
|
|
||||||
|
|
||||||
async def send(self, message: Union[Data, Iterable[Data]]) -> None:
|
|
||||||
"""
|
|
||||||
Send a message.
|
|
||||||
A string (:class:`str`) is sent as a `Text frame`_. A bytestring or
|
|
||||||
bytes-like object (:class:`bytes`, :class:`bytearray`, or
|
|
||||||
:class:`memoryview`) is sent as a `Binary frame`_.
|
|
||||||
.. _Text frame: https://tools.ietf.org/html/rfc6455#section-5.6
|
|
||||||
.. _Binary frame: https://tools.ietf.org/html/rfc6455#section-5.6
|
|
||||||
:meth:`send` also accepts an iterable of strings, bytestrings, or
|
|
||||||
bytes-like objects. In that case the message is fragmented. Each item
|
|
||||||
is treated as a message fragment and sent in its own frame. All items
|
|
||||||
must be of the same type, or else :meth:`send` will raise a
|
|
||||||
:exc:`TypeError` and the connection will be closed.
|
|
||||||
:meth:`send` rejects dict-like objects because this is often an error.
|
|
||||||
If you wish to send the keys of a dict-like object as fragments, call
|
|
||||||
its :meth:`~dict.keys` method and pass the result to :meth:`send`.
|
|
||||||
:raises TypeError: for unsupported inputs
|
|
||||||
"""
|
|
||||||
async with self.conn_mutex:
|
|
||||||
|
|
||||||
if self.connection.state in (CLOSED, CLOSING):
|
|
||||||
raise WebsocketClosed(
|
|
||||||
"Cannot write to websocket interface after it is closed."
|
|
||||||
)
|
|
||||||
if (not self.data_finished_fut) or self.data_finished_fut.done():
|
|
||||||
raise ServerError(
|
|
||||||
"Cannot write to websocket interface after it is finished."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unfragmented message -- this case must be handled first because
|
|
||||||
# strings and bytes-like objects are iterable.
|
|
||||||
|
|
||||||
if isinstance(message, str):
|
|
||||||
self.connection.send_text(message.encode("utf-8"))
|
|
||||||
await self.send_data(self.connection.data_to_send())
|
|
||||||
|
|
||||||
elif isinstance(message, (bytes, bytearray, memoryview)):
|
|
||||||
self.connection.send_binary(message)
|
|
||||||
await self.send_data(self.connection.data_to_send())
|
|
||||||
|
|
||||||
elif isinstance(message, Mapping):
|
|
||||||
# Catch a common mistake -- passing a dict to send().
|
|
||||||
raise TypeError("data is a dict-like object")
|
|
||||||
|
|
||||||
elif isinstance(message, Iterable):
|
|
||||||
# Fragmented message -- regular iterator.
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Fragmented websocket messages are not supported."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise TypeError("Websocket data must be bytes, str.")
|
|
||||||
|
|
||||||
async def ping(self, data: Optional[Data] = None) -> asyncio.Future:
|
|
||||||
"""
|
|
||||||
Send a ping.
|
|
||||||
Return an :class:`~asyncio.Future` that will be resolved when the
|
|
||||||
corresponding pong is received. You can ignore it if you don't intend
|
|
||||||
to wait.
|
|
||||||
A ping may serve as a keepalive or as a check that the remote endpoint
|
|
||||||
received all messages up to this point::
|
|
||||||
await pong_event = ws.ping()
|
|
||||||
await pong_event # only if you want to wait for the pong
|
|
||||||
By default, the ping contains four random bytes. This payload may be
|
|
||||||
overridden with the optional ``data`` argument which must be a string
|
|
||||||
(which will be encoded to UTF-8) or a bytes-like object.
|
|
||||||
"""
|
|
||||||
async with self.conn_mutex:
|
|
||||||
if self.connection.state in (CLOSED, CLOSING):
|
|
||||||
raise WebsocketClosed(
|
|
||||||
"Cannot send a ping when the websocket interface "
|
|
||||||
"is closed."
|
|
||||||
)
|
|
||||||
if (not self.io_proto) or (not self.io_proto.loop):
|
|
||||||
raise ServerError(
|
|
||||||
"Cannot send a ping when the websocket has no I/O "
|
|
||||||
"protocol attached."
|
|
||||||
)
|
|
||||||
if data is not None:
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = data.encode("utf-8")
|
|
||||||
elif isinstance(data, (bytearray, memoryview)):
|
|
||||||
data = bytes(data)
|
|
||||||
|
|
||||||
# Protect against duplicates if a payload is explicitly set.
|
|
||||||
if data in self.pings:
|
|
||||||
raise ValueError(
|
|
||||||
"already waiting for a pong with the same data"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate a unique random payload otherwise.
|
|
||||||
while data is None or data in self.pings:
|
|
||||||
data = struct.pack("!I", random.getrandbits(32))
|
|
||||||
|
|
||||||
self.pings[data] = self.io_proto.loop.create_future()
|
|
||||||
|
|
||||||
self.connection.send_ping(data)
|
|
||||||
await self.send_data(self.connection.data_to_send())
|
|
||||||
|
|
||||||
return asyncio.shield(self.pings[data])
|
|
||||||
|
|
||||||
async def pong(self, data: Data = b"") -> None:
|
|
||||||
"""
|
|
||||||
Send a pong.
|
|
||||||
An unsolicited pong may serve as a unidirectional heartbeat.
|
|
||||||
The payload may be set with the optional ``data`` argument which must
|
|
||||||
be a string (which will be encoded to UTF-8) or a bytes-like object.
|
|
||||||
"""
|
|
||||||
async with self.conn_mutex:
|
|
||||||
if self.connection.state in (CLOSED, CLOSING):
|
|
||||||
# Cannot send pong after transport is shutting down
|
|
||||||
return
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = data.encode("utf-8")
|
|
||||||
elif isinstance(data, (bytearray, memoryview)):
|
|
||||||
data = bytes(data)
|
|
||||||
self.connection.send_pong(data)
|
|
||||||
await self.send_data(self.connection.data_to_send())
|
|
||||||
|
|
||||||
async def send_data(self, data_to_send):
|
|
||||||
for data in data_to_send:
|
|
||||||
if data:
|
|
||||||
await self.io_proto.send(data)
|
|
||||||
else:
|
|
||||||
# Send an EOF - We don't actually send it,
|
|
||||||
# just trigger to autoclose the connection
|
|
||||||
if (
|
|
||||||
self.auto_closer_task
|
|
||||||
and not self.auto_closer_task.done()
|
|
||||||
and self.data_finished_fut
|
|
||||||
and not self.data_finished_fut.done()
|
|
||||||
):
|
|
||||||
# Auto-close the connection
|
|
||||||
self.data_finished_fut.set_result(None)
|
|
||||||
else:
|
|
||||||
# This will fail the connection appropriately
|
|
||||||
SanicProtocol.close(self.io_proto, timeout=1.0)
|
|
||||||
|
|
||||||
async def async_data_received(self, data_to_send, events_to_process):
|
|
||||||
if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0:
|
|
||||||
# receiving data can generate data to send (eg, pong for a ping)
|
|
||||||
# send connection.data_to_send()
|
|
||||||
await self.send_data(data_to_send)
|
|
||||||
if len(events_to_process) > 0:
|
|
||||||
await self.process_events(events_to_process)
|
|
||||||
|
|
||||||
def data_received(self, data):
|
|
||||||
self.connection.receive_data(data)
|
|
||||||
data_to_send = self.connection.data_to_send()
|
|
||||||
events_to_process = self.connection.events_received()
|
|
||||||
if len(data_to_send) > 0 or len(events_to_process) > 0:
|
|
||||||
asyncio.create_task(
|
|
||||||
self.async_data_received(data_to_send, events_to_process)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_eof_received(self, data_to_send, events_to_process):
|
|
||||||
# receiving EOF can generate data to send
|
|
||||||
# send connection.data_to_send()
|
|
||||||
if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0:
|
|
||||||
await self.send_data(data_to_send)
|
|
||||||
if len(events_to_process) > 0:
|
|
||||||
await self.process_events(events_to_process)
|
|
||||||
if self.recv_cancel:
|
|
||||||
self.recv_cancel.cancel()
|
|
||||||
if (
|
|
||||||
self.auto_closer_task
|
|
||||||
and not self.auto_closer_task.done()
|
|
||||||
and self.data_finished_fut
|
|
||||||
and not self.data_finished_fut.done()
|
|
||||||
):
|
|
||||||
# Auto-close the connection
|
|
||||||
self.data_finished_fut.set_result(None)
|
|
||||||
# Cancel the running handler if its waiting
|
|
||||||
else:
|
|
||||||
# This will fail the connection appropriately
|
|
||||||
SanicProtocol.close(self.io_proto, timeout=1.0)
|
|
||||||
|
|
||||||
def eof_received(self) -> Optional[bool]:
|
|
||||||
self.connection.receive_eof()
|
|
||||||
data_to_send = self.connection.data_to_send()
|
|
||||||
events_to_process = self.connection.events_received()
|
|
||||||
asyncio.create_task(
|
|
||||||
self.async_eof_received(data_to_send, events_to_process)
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
|
||||||
"""
|
|
||||||
The WebSocket Connection is Closed.
|
|
||||||
"""
|
|
||||||
if not self.connection.state == CLOSED:
|
|
||||||
# signal to the websocket connection handler
|
|
||||||
# we've lost the connection
|
|
||||||
self.connection.fail(code=1006)
|
|
||||||
self.connection.state = CLOSED
|
|
||||||
|
|
||||||
self.abort_pings()
|
|
||||||
if self.connection_lost_waiter:
|
|
||||||
self.connection_lost_waiter.set_result(None)
|
|
||||||
107
sanic/signals.py
107
sanic/signals.py
@@ -10,39 +10,13 @@ from sanic_routing.exceptions import NotFound # type: ignore
|
|||||||
from sanic_routing.utils import path_to_parts # type: ignore
|
from sanic_routing.utils import path_to_parts # type: ignore
|
||||||
|
|
||||||
from sanic.exceptions import InvalidSignal
|
from sanic.exceptions import InvalidSignal
|
||||||
from sanic.log import error_logger, logger
|
|
||||||
from sanic.models.handler_types import SignalHandler
|
from sanic.models.handler_types import SignalHandler
|
||||||
|
|
||||||
|
|
||||||
RESERVED_NAMESPACES = {
|
RESERVED_NAMESPACES = (
|
||||||
"server": (
|
"server",
|
||||||
# "server.main.start",
|
"http",
|
||||||
# "server.main.stop",
|
)
|
||||||
"server.init.before",
|
|
||||||
"server.init.after",
|
|
||||||
"server.shutdown.before",
|
|
||||||
"server.shutdown.after",
|
|
||||||
),
|
|
||||||
"http": (
|
|
||||||
"http.lifecycle.begin",
|
|
||||||
"http.lifecycle.complete",
|
|
||||||
"http.lifecycle.exception",
|
|
||||||
"http.lifecycle.handle",
|
|
||||||
"http.lifecycle.read_body",
|
|
||||||
"http.lifecycle.read_head",
|
|
||||||
"http.lifecycle.request",
|
|
||||||
"http.lifecycle.response",
|
|
||||||
"http.routing.after",
|
|
||||||
"http.routing.before",
|
|
||||||
"http.lifecycle.send",
|
|
||||||
"http.middleware.after",
|
|
||||||
"http.middleware.before",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _blank():
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class Signal(Route):
|
class Signal(Route):
|
||||||
@@ -74,7 +48,7 @@ class SignalRouter(BaseRouter):
|
|||||||
f".{event}",
|
f".{event}",
|
||||||
self.DEFAULT_METHOD,
|
self.DEFAULT_METHOD,
|
||||||
self,
|
self,
|
||||||
{"__params__": {}, "__matches__": {}},
|
{"__params__": {}},
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
except NotFound:
|
except NotFound:
|
||||||
@@ -85,18 +59,7 @@ class SignalRouter(BaseRouter):
|
|||||||
terms.append(extra)
|
terms.append(extra)
|
||||||
raise NotFound(message % tuple(terms))
|
raise NotFound(message % tuple(terms))
|
||||||
|
|
||||||
# Regex routes evaluate and can extract params directly. They are set
|
params = param_basket.pop("__params__")
|
||||||
# on param_basket["__params__"]
|
|
||||||
params = param_basket["__params__"]
|
|
||||||
if not params:
|
|
||||||
# If param_basket["__params__"] does not exist, we might have
|
|
||||||
# param_basket["__matches__"], which are indexed based matches
|
|
||||||
# on path segments. They should already be cast types.
|
|
||||||
params = {
|
|
||||||
param.name: param_basket["__matches__"][idx]
|
|
||||||
for idx, param in group.params.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
return group, [route.handler for route in group], params
|
return group, [route.handler for route in group], params
|
||||||
|
|
||||||
async def _dispatch(
|
async def _dispatch(
|
||||||
@@ -104,18 +67,8 @@ class SignalRouter(BaseRouter):
|
|||||||
event: str,
|
event: str,
|
||||||
context: Optional[Dict[str, Any]] = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
condition: Optional[Dict[str, str]] = None,
|
condition: Optional[Dict[str, str]] = None,
|
||||||
fail_not_found: bool = True,
|
) -> None:
|
||||||
reverse: bool = False,
|
group, handlers, params = self.get(event, condition=condition)
|
||||||
) -> Any:
|
|
||||||
try:
|
|
||||||
group, handlers, params = self.get(event, condition=condition)
|
|
||||||
except NotFound as e:
|
|
||||||
if fail_not_found:
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
if self.ctx.app.debug:
|
|
||||||
error_logger.warning(str(e))
|
|
||||||
return None
|
|
||||||
|
|
||||||
events = [signal.ctx.event for signal in group]
|
events = [signal.ctx.event for signal in group]
|
||||||
for signal_event in events:
|
for signal_event in events:
|
||||||
@@ -123,19 +76,12 @@ class SignalRouter(BaseRouter):
|
|||||||
if context:
|
if context:
|
||||||
params.update(context)
|
params.update(context)
|
||||||
|
|
||||||
if not reverse:
|
|
||||||
handlers = handlers[::-1]
|
|
||||||
try:
|
try:
|
||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
if condition is None or condition == handler.__requirements__:
|
if condition is None or condition == handler.__requirements__:
|
||||||
maybe_coroutine = handler(**params)
|
maybe_coroutine = handler(**params)
|
||||||
if isawaitable(maybe_coroutine):
|
if isawaitable(maybe_coroutine):
|
||||||
retval = await maybe_coroutine
|
await maybe_coroutine
|
||||||
if retval:
|
|
||||||
return retval
|
|
||||||
elif maybe_coroutine:
|
|
||||||
return maybe_coroutine
|
|
||||||
return None
|
|
||||||
finally:
|
finally:
|
||||||
for signal_event in events:
|
for signal_event in events:
|
||||||
signal_event.clear()
|
signal_event.clear()
|
||||||
@@ -146,23 +92,14 @@ class SignalRouter(BaseRouter):
|
|||||||
*,
|
*,
|
||||||
context: Optional[Dict[str, Any]] = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
condition: Optional[Dict[str, str]] = None,
|
condition: Optional[Dict[str, str]] = None,
|
||||||
fail_not_found: bool = True,
|
) -> asyncio.Task:
|
||||||
inline: bool = False,
|
task = self.ctx.loop.create_task(
|
||||||
reverse: bool = False,
|
self._dispatch(
|
||||||
) -> Union[asyncio.Task, Any]:
|
event,
|
||||||
dispatch = self._dispatch(
|
context=context,
|
||||||
event,
|
condition=condition,
|
||||||
context=context,
|
)
|
||||||
condition=condition,
|
|
||||||
fail_not_found=fail_not_found and inline,
|
|
||||||
reverse=reverse,
|
|
||||||
)
|
)
|
||||||
logger.debug(f"Dispatching signal: {event}")
|
|
||||||
|
|
||||||
if inline:
|
|
||||||
return await dispatch
|
|
||||||
|
|
||||||
task = asyncio.get_running_loop().create_task(dispatch)
|
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
@@ -188,9 +125,7 @@ class SignalRouter(BaseRouter):
|
|||||||
append=True,
|
append=True,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
|
def finalize(self, do_compile: bool = True):
|
||||||
self.add(_blank, "sanic.__signal__.__init__")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ctx.loop = asyncio.get_running_loop()
|
self.ctx.loop = asyncio.get_running_loop()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
@@ -199,7 +134,7 @@ class SignalRouter(BaseRouter):
|
|||||||
for signal in self.routes:
|
for signal in self.routes:
|
||||||
signal.ctx.event = asyncio.Event()
|
signal.ctx.event = asyncio.Event()
|
||||||
|
|
||||||
return super().finalize(do_compile=do_compile, do_optimize=do_optimize)
|
return super().finalize(do_compile=do_compile)
|
||||||
|
|
||||||
def _build_event_parts(self, event: str) -> Tuple[str, str, str]:
|
def _build_event_parts(self, event: str) -> Tuple[str, str, str]:
|
||||||
parts = path_to_parts(event, self.delimiter)
|
parts = path_to_parts(event, self.delimiter)
|
||||||
@@ -210,11 +145,7 @@ class SignalRouter(BaseRouter):
|
|||||||
):
|
):
|
||||||
raise InvalidSignal("Invalid signal event: %s" % event)
|
raise InvalidSignal("Invalid signal event: %s" % event)
|
||||||
|
|
||||||
if (
|
if parts[0] in RESERVED_NAMESPACES:
|
||||||
parts[0] in RESERVED_NAMESPACES
|
|
||||||
and event not in RESERVED_NAMESPACES[parts[0]]
|
|
||||||
and not (parts[2].startswith("<") and parts[2].endswith(">"))
|
|
||||||
):
|
|
||||||
raise InvalidSignal(
|
raise InvalidSignal(
|
||||||
"Cannot declare reserved signal event: %s" % event
|
"Cannot declare reserved signal event: %s" % event
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.exceptions import SanicException
|
|
||||||
from sanic.response import redirect
|
|
||||||
|
|
||||||
|
|
||||||
def create_simple_server(directory: Path):
|
|
||||||
if not directory.is_dir():
|
|
||||||
raise SanicException(
|
|
||||||
"Cannot setup Sanic Simple Server without a path to a directory"
|
|
||||||
)
|
|
||||||
|
|
||||||
app = Sanic("SimpleServer")
|
|
||||||
app.static("/", directory, name="main")
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
def index(_):
|
|
||||||
return redirect(app.url_for("main", filename="index.html"))
|
|
||||||
|
|
||||||
return app
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from .meta import TouchUpMeta
|
|
||||||
from .service import TouchUp
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"TouchUp",
|
|
||||||
"TouchUpMeta",
|
|
||||||
)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
from sanic.exceptions import SanicException
|
|
||||||
|
|
||||||
from .service import TouchUp
|
|
||||||
|
|
||||||
|
|
||||||
class TouchUpMeta(type):
|
|
||||||
def __new__(cls, name, bases, attrs, **kwargs):
|
|
||||||
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
||||||
|
|
||||||
methods = attrs.get("__touchup__")
|
|
||||||
attrs["__touched__"] = False
|
|
||||||
if methods:
|
|
||||||
|
|
||||||
for method in methods:
|
|
||||||
if method not in attrs:
|
|
||||||
raise SanicException(
|
|
||||||
"Cannot perform touchup on non-existent method: "
|
|
||||||
f"{name}.{method}"
|
|
||||||
)
|
|
||||||
TouchUp.register(gen_class, method)
|
|
||||||
|
|
||||||
return gen_class
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from .base import BaseScheme
|
|
||||||
from .ode import OptionalDispatchEvent # noqa
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("BaseScheme",)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Set, Type
|
|
||||||
|
|
||||||
|
|
||||||
class BaseScheme(ABC):
|
|
||||||
ident: str
|
|
||||||
_registry: Set[Type] = set()
|
|
||||||
|
|
||||||
def __init__(self, app) -> None:
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def run(self, method, module_globals) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
def __init_subclass__(cls):
|
|
||||||
BaseScheme._registry.add(cls)
|
|
||||||
|
|
||||||
def __call__(self, method, module_globals):
|
|
||||||
return self.run(method, module_globals)
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
from ast import Attribute, Await, Dict, Expr, NodeTransformer, parse
|
|
||||||
from inspect import getsource
|
|
||||||
from textwrap import dedent
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from sanic.log import logger
|
|
||||||
|
|
||||||
from .base import BaseScheme
|
|
||||||
|
|
||||||
|
|
||||||
class OptionalDispatchEvent(BaseScheme):
|
|
||||||
ident = "ODE"
|
|
||||||
|
|
||||||
def __init__(self, app) -> None:
|
|
||||||
super().__init__(app)
|
|
||||||
|
|
||||||
self._registered_events = [
|
|
||||||
signal.path for signal in app.signal_router.routes
|
|
||||||
]
|
|
||||||
|
|
||||||
def run(self, method, module_globals):
|
|
||||||
raw_source = getsource(method)
|
|
||||||
src = dedent(raw_source)
|
|
||||||
tree = parse(src)
|
|
||||||
node = RemoveDispatch(self._registered_events).visit(tree)
|
|
||||||
compiled_src = compile(node, method.__name__, "exec")
|
|
||||||
exec_locals: Dict[str, Any] = {}
|
|
||||||
exec(compiled_src, module_globals, exec_locals) # nosec
|
|
||||||
|
|
||||||
return exec_locals[method.__name__]
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveDispatch(NodeTransformer):
|
|
||||||
def __init__(self, registered_events) -> None:
|
|
||||||
self._registered_events = registered_events
|
|
||||||
|
|
||||||
def visit_Expr(self, node: Expr) -> Any:
|
|
||||||
call = node.value
|
|
||||||
if isinstance(call, Await):
|
|
||||||
call = call.value
|
|
||||||
|
|
||||||
func = getattr(call, "func", None)
|
|
||||||
args = getattr(call, "args", None)
|
|
||||||
if not func or not args:
|
|
||||||
return node
|
|
||||||
|
|
||||||
if isinstance(func, Attribute) and func.attr == "dispatch":
|
|
||||||
event = args[0]
|
|
||||||
if hasattr(event, "s"):
|
|
||||||
event_name = getattr(event, "value", event.s)
|
|
||||||
if self._not_registered(event_name):
|
|
||||||
logger.debug(f"Disabling event: {event_name}")
|
|
||||||
return None
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _not_registered(self, event_name):
|
|
||||||
dynamic = []
|
|
||||||
for event in self._registered_events:
|
|
||||||
if event.endswith(">"):
|
|
||||||
namespace_concern, _ = event.rsplit(".", 1)
|
|
||||||
dynamic.append(namespace_concern)
|
|
||||||
|
|
||||||
namespace_concern, _ = event_name.rsplit(".", 1)
|
|
||||||
return (
|
|
||||||
event_name not in self._registered_events
|
|
||||||
and namespace_concern not in dynamic
|
|
||||||
)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
from inspect import getmembers, getmodule
|
|
||||||
from typing import Set, Tuple, Type
|
|
||||||
|
|
||||||
from .schemes import BaseScheme
|
|
||||||
|
|
||||||
|
|
||||||
class TouchUp:
|
|
||||||
_registry: Set[Tuple[Type, str]] = set()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def run(cls, app):
|
|
||||||
for target, method_name in cls._registry:
|
|
||||||
method = getattr(target, method_name)
|
|
||||||
|
|
||||||
if app.test_mode:
|
|
||||||
placeholder = f"_{method_name}"
|
|
||||||
if hasattr(target, placeholder):
|
|
||||||
method = getattr(target, placeholder)
|
|
||||||
else:
|
|
||||||
setattr(target, placeholder, method)
|
|
||||||
|
|
||||||
module = getmodule(target)
|
|
||||||
module_globals = dict(getmembers(module))
|
|
||||||
|
|
||||||
for scheme in BaseScheme._registry:
|
|
||||||
modified = scheme(app)(method, module_globals)
|
|
||||||
setattr(target, method_name, modified)
|
|
||||||
|
|
||||||
target.__touched__ = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register(cls, target, method_name):
|
|
||||||
cls._registry.add((target, method_name))
|
|
||||||
@@ -105,7 +105,6 @@ def load_module_from_file_location(
|
|||||||
_mod_spec = spec_from_file_location(
|
_mod_spec = spec_from_file_location(
|
||||||
name, location, *args, **kwargs
|
name, location, *args, **kwargs
|
||||||
)
|
)
|
||||||
assert _mod_spec is not None # type assertion for mypy
|
|
||||||
module = module_from_spec(_mod_spec)
|
module = module_from_spec(_mod_spec)
|
||||||
_mod_spec.loader.exec_module(module) # type: ignore
|
_mod_spec.loader.exec_module(module) # type: ignore
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
from __future__ import annotations
|
from typing import Any, Callable, List
|
||||||
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
Iterable,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import InvalidUsage
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.models.handler_types import RouteHandler
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.blueprints import Blueprint
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethodView:
|
class HTTPMethodView:
|
||||||
@@ -57,37 +40,12 @@ class HTTPMethodView:
|
|||||||
|
|
||||||
decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = []
|
decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = []
|
||||||
|
|
||||||
def __init_subclass__(
|
|
||||||
cls,
|
|
||||||
attach: Optional[Union[Sanic, Blueprint]] = None,
|
|
||||||
uri: str = "",
|
|
||||||
methods: Iterable[str] = frozenset({"GET"}),
|
|
||||||
host: Optional[str] = None,
|
|
||||||
strict_slashes: Optional[bool] = None,
|
|
||||||
version: Optional[int] = None,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
stream: bool = False,
|
|
||||||
version_prefix: str = "/v",
|
|
||||||
) -> None:
|
|
||||||
if attach:
|
|
||||||
cls.attach(
|
|
||||||
attach,
|
|
||||||
uri=uri,
|
|
||||||
methods=methods,
|
|
||||||
host=host,
|
|
||||||
strict_slashes=strict_slashes,
|
|
||||||
version=version,
|
|
||||||
name=name,
|
|
||||||
stream=stream,
|
|
||||||
version_prefix=version_prefix,
|
|
||||||
)
|
|
||||||
|
|
||||||
def dispatch_request(self, request, *args, **kwargs):
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, *class_args: Any, **class_kwargs: Any) -> RouteHandler:
|
def as_view(cls, *class_args, **class_kwargs):
|
||||||
"""Return view function for use with the routing system, that
|
"""Return view function for use with the routing system, that
|
||||||
dispatches request to appropriate handler method.
|
dispatches request to appropriate handler method.
|
||||||
"""
|
"""
|
||||||
@@ -101,37 +59,12 @@ class HTTPMethodView:
|
|||||||
for decorator in cls.decorators:
|
for decorator in cls.decorators:
|
||||||
view = decorator(view)
|
view = decorator(view)
|
||||||
|
|
||||||
view.view_class = cls # type: ignore
|
view.view_class = cls
|
||||||
view.__doc__ = cls.__doc__
|
view.__doc__ = cls.__doc__
|
||||||
view.__module__ = cls.__module__
|
view.__module__ = cls.__module__
|
||||||
view.__name__ = cls.__name__
|
view.__name__ = cls.__name__
|
||||||
return view
|
return view
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def attach(
|
|
||||||
cls,
|
|
||||||
to: Union[Sanic, Blueprint],
|
|
||||||
uri: str,
|
|
||||||
methods: Iterable[str] = frozenset({"GET"}),
|
|
||||||
host: Optional[str] = None,
|
|
||||||
strict_slashes: Optional[bool] = None,
|
|
||||||
version: Optional[int] = None,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
stream: bool = False,
|
|
||||||
version_prefix: str = "/v",
|
|
||||||
) -> None:
|
|
||||||
to.add_route(
|
|
||||||
cls.as_view(),
|
|
||||||
uri=uri,
|
|
||||||
methods=methods,
|
|
||||||
host=host,
|
|
||||||
strict_slashes=strict_slashes,
|
|
||||||
version=version,
|
|
||||||
name=name,
|
|
||||||
stream=stream,
|
|
||||||
version_prefix=version_prefix,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def stream(func):
|
def stream(func):
|
||||||
func.is_stream = True
|
func.is_stream = True
|
||||||
@@ -158,11 +91,6 @@ class CompositionView:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
self.name = self.__class__.__name__
|
self.name = self.__class__.__name__
|
||||||
warn(
|
|
||||||
"CompositionView has been deprecated and will be removed in "
|
|
||||||
"v21.12. Please update your view to HTTPMethodView.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __name__(self):
|
def __name__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
184
sanic/websocket.py
Normal file
184
sanic/websocket.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
MutableMapping,
|
||||||
|
Optional,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
from httptools import HttpParserUpgrade # type: ignore
|
||||||
|
from websockets import ( # type: ignore
|
||||||
|
ConnectionClosed,
|
||||||
|
InvalidHandshake,
|
||||||
|
WebSocketCommonProtocol,
|
||||||
|
handshake,
|
||||||
|
)
|
||||||
|
|
||||||
|
from sanic.exceptions import InvalidUsage
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["ConnectionClosed", "WebSocketProtocol", "WebSocketConnection"]
|
||||||
|
|
||||||
|
ASIMessage = MutableMapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketProtocol(HttpProtocol):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
websocket_timeout=10,
|
||||||
|
websocket_max_size=None,
|
||||||
|
websocket_max_queue=None,
|
||||||
|
websocket_read_limit=2 ** 16,
|
||||||
|
websocket_write_limit=2 ** 16,
|
||||||
|
websocket_ping_interval=20,
|
||||||
|
websocket_ping_timeout=20,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.websocket = None
|
||||||
|
# self.app = None
|
||||||
|
self.websocket_timeout = websocket_timeout
|
||||||
|
self.websocket_max_size = websocket_max_size
|
||||||
|
self.websocket_max_queue = websocket_max_queue
|
||||||
|
self.websocket_read_limit = websocket_read_limit
|
||||||
|
self.websocket_write_limit = websocket_write_limit
|
||||||
|
self.websocket_ping_interval = websocket_ping_interval
|
||||||
|
self.websocket_ping_timeout = websocket_ping_timeout
|
||||||
|
|
||||||
|
# timeouts make no sense for websocket routes
|
||||||
|
def request_timeout_callback(self):
|
||||||
|
if self.websocket is None:
|
||||||
|
super().request_timeout_callback()
|
||||||
|
|
||||||
|
def response_timeout_callback(self):
|
||||||
|
if self.websocket is None:
|
||||||
|
super().response_timeout_callback()
|
||||||
|
|
||||||
|
def keep_alive_timeout_callback(self):
|
||||||
|
if self.websocket is None:
|
||||||
|
super().keep_alive_timeout_callback()
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
if self.websocket is not None:
|
||||||
|
self.websocket.connection_lost(exc)
|
||||||
|
super().connection_lost(exc)
|
||||||
|
|
||||||
|
def data_received(self, data):
|
||||||
|
if self.websocket is not None:
|
||||||
|
# pass the data to the websocket protocol
|
||||||
|
self.websocket.data_received(data)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
super().data_received(data)
|
||||||
|
except HttpParserUpgrade:
|
||||||
|
# this is okay, it just indicates we've got an upgrade request
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write_response(self, response):
|
||||||
|
if self.websocket is not None:
|
||||||
|
# websocket requests do not write a response
|
||||||
|
self.transport.close()
|
||||||
|
else:
|
||||||
|
super().write_response(response)
|
||||||
|
|
||||||
|
async def websocket_handshake(self, request, subprotocols=None):
|
||||||
|
# let the websockets package do the handshake with the client
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = handshake.check_request(request.headers)
|
||||||
|
handshake.build_response(headers, key)
|
||||||
|
except InvalidHandshake:
|
||||||
|
raise InvalidUsage("Invalid websocket request")
|
||||||
|
|
||||||
|
subprotocol = None
|
||||||
|
if subprotocols and "Sec-Websocket-Protocol" in request.headers:
|
||||||
|
# select a subprotocol
|
||||||
|
client_subprotocols = [
|
||||||
|
p.strip()
|
||||||
|
for p in request.headers["Sec-Websocket-Protocol"].split(",")
|
||||||
|
]
|
||||||
|
for p in client_subprotocols:
|
||||||
|
if p in subprotocols:
|
||||||
|
subprotocol = p
|
||||||
|
headers["Sec-Websocket-Protocol"] = subprotocol
|
||||||
|
break
|
||||||
|
|
||||||
|
# write the 101 response back to the client
|
||||||
|
rv = b"HTTP/1.1 101 Switching Protocols\r\n"
|
||||||
|
for k, v in headers.items():
|
||||||
|
rv += k.encode("utf-8") + b": " + v.encode("utf-8") + b"\r\n"
|
||||||
|
rv += b"\r\n"
|
||||||
|
request.transport.write(rv)
|
||||||
|
|
||||||
|
# hook up the websocket protocol
|
||||||
|
self.websocket = WebSocketCommonProtocol(
|
||||||
|
close_timeout=self.websocket_timeout,
|
||||||
|
max_size=self.websocket_max_size,
|
||||||
|
max_queue=self.websocket_max_queue,
|
||||||
|
read_limit=self.websocket_read_limit,
|
||||||
|
write_limit=self.websocket_write_limit,
|
||||||
|
ping_interval=self.websocket_ping_interval,
|
||||||
|
ping_timeout=self.websocket_ping_timeout,
|
||||||
|
)
|
||||||
|
# Following two lines are required for websockets 8.x
|
||||||
|
self.websocket.is_client = False
|
||||||
|
self.websocket.side = "server"
|
||||||
|
self.websocket.subprotocol = subprotocol
|
||||||
|
self.websocket.connection_made(request.transport)
|
||||||
|
self.websocket.connection_open()
|
||||||
|
return self.websocket
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketConnection:
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# - Implement ping/pong
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
send: Callable[[ASIMessage], Awaitable[None]],
|
||||||
|
receive: Callable[[], Awaitable[ASIMessage]],
|
||||||
|
subprotocols: Optional[List[str]] = None,
|
||||||
|
) -> None:
|
||||||
|
self._send = send
|
||||||
|
self._receive = receive
|
||||||
|
self.subprotocols = subprotocols or []
|
||||||
|
|
||||||
|
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
|
||||||
|
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
|
||||||
|
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
message.update({"bytes": data})
|
||||||
|
else:
|
||||||
|
message.update({"text": str(data)})
|
||||||
|
|
||||||
|
await self._send(message)
|
||||||
|
|
||||||
|
async def recv(self, *args, **kwargs) -> Optional[str]:
|
||||||
|
message = await self._receive()
|
||||||
|
|
||||||
|
if message["type"] == "websocket.receive":
|
||||||
|
return message["text"]
|
||||||
|
elif message["type"] == "websocket.disconnect":
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
receive = recv
|
||||||
|
|
||||||
|
async def accept(self) -> None:
|
||||||
|
await self._send(
|
||||||
|
{
|
||||||
|
"type": "websocket.accept",
|
||||||
|
"subprotocol": ",".join(list(self.subprotocols)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
pass
|
||||||
@@ -8,8 +8,8 @@ import traceback
|
|||||||
from gunicorn.workers import base # type: ignore
|
from gunicorn.workers import base # type: ignore
|
||||||
|
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
from sanic.server import HttpProtocol, Signal, serve
|
from sanic.server import HttpProtocol, Signal, serve, trigger_events
|
||||||
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
from sanic.websocket import WebSocketProtocol
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -68,10 +68,10 @@ class GunicornWorker(base.Worker):
|
|||||||
)
|
)
|
||||||
self._server_settings["signal"] = self.signal
|
self._server_settings["signal"] = self.signal
|
||||||
self._server_settings.pop("sock")
|
self._server_settings.pop("sock")
|
||||||
self._await(self.app.callable._startup())
|
trigger_events(
|
||||||
self._await(
|
self._server_settings.get("before_start", []), self.loop
|
||||||
self.app.callable._server_event("init", "before", loop=self.loop)
|
|
||||||
)
|
)
|
||||||
|
self._server_settings["before_start"] = ()
|
||||||
|
|
||||||
main_start = self._server_settings.pop("main_start", None)
|
main_start = self._server_settings.pop("main_start", None)
|
||||||
main_stop = self._server_settings.pop("main_stop", None)
|
main_stop = self._server_settings.pop("main_stop", None)
|
||||||
@@ -82,29 +82,24 @@ class GunicornWorker(base.Worker):
|
|||||||
"with GunicornWorker"
|
"with GunicornWorker"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
|
||||||
try:
|
try:
|
||||||
self._await(self._run())
|
self.loop.run_until_complete(self._runner)
|
||||||
self.app.callable.is_running = True
|
self.app.callable.is_running = True
|
||||||
self._await(
|
trigger_events(
|
||||||
self.app.callable._server_event(
|
self._server_settings.get("after_start", []), self.loop
|
||||||
"init", "after", loop=self.loop
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.loop.run_until_complete(self._check_alive())
|
self.loop.run_until_complete(self._check_alive())
|
||||||
self._await(
|
trigger_events(
|
||||||
self.app.callable._server_event(
|
self._server_settings.get("before_stop", []), self.loop
|
||||||
"shutdown", "before", loop=self.loop
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.loop.run_until_complete(self.close())
|
self.loop.run_until_complete(self.close())
|
||||||
except BaseException:
|
except BaseException:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
self._await(
|
trigger_events(
|
||||||
self.app.callable._server_event(
|
self._server_settings.get("after_stop", []), self.loop
|
||||||
"shutdown", "after", loop=self.loop
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -142,11 +137,14 @@ class GunicornWorker(base.Worker):
|
|||||||
|
|
||||||
# Force close non-idle connection after waiting for
|
# Force close non-idle connection after waiting for
|
||||||
# graceful_shutdown_timeout
|
# graceful_shutdown_timeout
|
||||||
|
coros = []
|
||||||
for conn in self.connections:
|
for conn in self.connections:
|
||||||
if hasattr(conn, "websocket") and conn.websocket:
|
if hasattr(conn, "websocket") and conn.websocket:
|
||||||
conn.websocket.fail_connection(code=1001)
|
coros.append(conn.websocket.close_connection())
|
||||||
else:
|
else:
|
||||||
conn.abort()
|
conn.close()
|
||||||
|
_shutdown = asyncio.gather(*coros, loop=self.loop)
|
||||||
|
await _shutdown
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
for sock in self.sockets:
|
for sock in self.sockets:
|
||||||
@@ -240,7 +238,3 @@ class GunicornWorker(base.Worker):
|
|||||||
self.exit_code = 1
|
self.exit_code = 1
|
||||||
self.cfg.worker_abort(self)
|
self.cfg.worker_abort(self)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def _await(self, coro):
|
|
||||||
fut = asyncio.ensure_future(coro, loop=self.loop)
|
|
||||||
self.loop.run_until_complete(fut)
|
|
||||||
|
|||||||
35
setup.py
35
setup.py
@@ -81,63 +81,60 @@ env_dependency = (
|
|||||||
)
|
)
|
||||||
ujson = "ujson>=1.35" + env_dependency
|
ujson = "ujson>=1.35" + env_dependency
|
||||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||||
types_ujson = "types-ujson" + env_dependency
|
|
||||||
requirements = [
|
requirements = [
|
||||||
"sanic-routing~=0.7",
|
"sanic-routing>=0.6.0",
|
||||||
"httptools>=0.0.10",
|
"httptools>=0.0.10",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
"aiofiles>=0.6.0",
|
"aiofiles>=0.6.0",
|
||||||
"websockets>=10.0",
|
"websockets>=8.1,<9.0",
|
||||||
"multidict>=5.0,<6.0",
|
"multidict>=5.0,<6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing>=0.7.0",
|
"sanic-testing",
|
||||||
"pytest==5.2.1",
|
"pytest==5.2.1",
|
||||||
"coverage==5.3",
|
"multidict>=5.0,<6.0",
|
||||||
"gunicorn==20.0.4",
|
"gunicorn==20.0.4",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
|
uvloop,
|
||||||
|
ujson,
|
||||||
"pytest-sanic",
|
"pytest-sanic",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-benchmark",
|
"pytest-benchmark",
|
||||||
"chardet==3.*",
|
|
||||||
"flake8",
|
|
||||||
"black",
|
|
||||||
"isort>=5.0.0",
|
|
||||||
"bandit",
|
|
||||||
"mypy>=0.901",
|
|
||||||
"docutils",
|
|
||||||
"pygments",
|
|
||||||
"uvicorn<0.15.0",
|
|
||||||
types_ujson,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
docs_require = [
|
docs_require = [
|
||||||
"sphinx>=2.1.2",
|
"sphinx>=2.1.2",
|
||||||
"sphinx_rtd_theme>=0.4.3",
|
"sphinx_rtd_theme",
|
||||||
|
"recommonmark>=0.5.0",
|
||||||
"docutils",
|
"docutils",
|
||||||
"pygments",
|
"pygments",
|
||||||
"m2r2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
dev_require = tests_require + [
|
dev_require = tests_require + [
|
||||||
|
"aiofiles",
|
||||||
"tox",
|
"tox",
|
||||||
|
"black",
|
||||||
|
"flake8",
|
||||||
|
"bandit",
|
||||||
"towncrier",
|
"towncrier",
|
||||||
]
|
]
|
||||||
|
|
||||||
all_require = list(set(dev_require + docs_require))
|
all_require = dev_require + docs_require
|
||||||
|
|
||||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||||
print("Installing without uJSON")
|
print("Installing without uJSON")
|
||||||
requirements.remove(ujson)
|
requirements.remove(ujson)
|
||||||
tests_require.remove(types_ujson)
|
tests_require.remove(ujson)
|
||||||
|
|
||||||
# 'nt' means windows OS
|
# 'nt' means windows OS
|
||||||
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
|
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
|
||||||
print("Installing without uvLoop")
|
print("Installing without uvLoop")
|
||||||
requirements.remove(uvloop)
|
requirements.remove(uvloop)
|
||||||
|
tests_require.remove(uvloop)
|
||||||
|
|
||||||
extras_require = {
|
extras_require = {
|
||||||
"test": tests_require,
|
"test": tests_require,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
@@ -11,15 +9,12 @@ from typing import Tuple
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic_routing.exceptions import RouteExists
|
from sanic_routing.exceptions import RouteExists
|
||||||
from sanic_testing.testing import PORT
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
from sanic.touchup.service import TouchUp
|
|
||||||
|
|
||||||
|
|
||||||
slugify = re.compile(r"[^a-zA-Z0-9_\-]")
|
|
||||||
random.seed("Pack my box with five dozen liquor jugs.")
|
random.seed("Pack my box with five dozen liquor jugs.")
|
||||||
Sanic.test_mode = True
|
Sanic.test_mode = True
|
||||||
|
|
||||||
@@ -27,6 +22,11 @@ if sys.platform in ["win32", "cygwin"]:
|
|||||||
collect_ignore = ["test_worker.py"]
|
collect_ignore = ["test_worker.py"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def caplog(caplog):
|
||||||
|
yield caplog
|
||||||
|
|
||||||
|
|
||||||
async def _handler(request):
|
async def _handler(request):
|
||||||
"""
|
"""
|
||||||
Dummy placeholder method used for route resolver when creating a new
|
Dummy placeholder method used for route resolver when creating a new
|
||||||
@@ -40,32 +40,33 @@ async def _handler(request):
|
|||||||
|
|
||||||
|
|
||||||
TYPE_TO_GENERATOR_MAP = {
|
TYPE_TO_GENERATOR_MAP = {
|
||||||
"str": lambda: "".join(
|
"string": lambda: "".join(
|
||||||
[random.choice(string.ascii_lowercase) for _ in range(4)]
|
[random.choice(string.ascii_lowercase) for _ in range(4)]
|
||||||
),
|
),
|
||||||
"int": lambda: random.choice(range(1000000)),
|
"int": lambda: random.choice(range(1000000)),
|
||||||
"float": lambda: random.random(),
|
"number": lambda: random.random(),
|
||||||
"alpha": lambda: "".join(
|
"alpha": lambda: "".join(
|
||||||
[random.choice(string.ascii_lowercase) for _ in range(4)]
|
[random.choice(string.ascii_lowercase) for _ in range(4)]
|
||||||
),
|
),
|
||||||
"uuid": lambda: str(uuid.uuid1()),
|
"uuid": lambda: str(uuid.uuid1()),
|
||||||
}
|
}
|
||||||
|
|
||||||
CACHE = {}
|
|
||||||
|
|
||||||
|
|
||||||
class RouteStringGenerator:
|
class RouteStringGenerator:
|
||||||
|
|
||||||
ROUTE_COUNT_PER_DEPTH = 100
|
ROUTE_COUNT_PER_DEPTH = 100
|
||||||
HTTP_METHODS = HTTP_METHODS
|
HTTP_METHODS = HTTP_METHODS
|
||||||
ROUTE_PARAM_TYPES = ["str", "int", "float", "alpha", "uuid"]
|
ROUTE_PARAM_TYPES = ["string", "int", "number", "alpha", "uuid"]
|
||||||
|
|
||||||
def generate_random_direct_route(self, max_route_depth=4):
|
def generate_random_direct_route(self, max_route_depth=4):
|
||||||
routes = []
|
routes = []
|
||||||
for depth in range(1, max_route_depth + 1):
|
for depth in range(1, max_route_depth + 1):
|
||||||
for _ in range(self.ROUTE_COUNT_PER_DEPTH):
|
for _ in range(self.ROUTE_COUNT_PER_DEPTH):
|
||||||
route = "/".join(
|
route = "/".join(
|
||||||
[TYPE_TO_GENERATOR_MAP.get("str")() for _ in range(depth)]
|
[
|
||||||
|
TYPE_TO_GENERATOR_MAP.get("string")()
|
||||||
|
for _ in range(depth)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
route = route.replace(".", "", -1)
|
route = route.replace(".", "", -1)
|
||||||
route_detail = (random.choice(self.HTTP_METHODS), route)
|
route_detail = (random.choice(self.HTTP_METHODS), route)
|
||||||
@@ -81,7 +82,7 @@ class RouteStringGenerator:
|
|||||||
new_route_part = "/".join(
|
new_route_part = "/".join(
|
||||||
[
|
[
|
||||||
"<{}:{}>".format(
|
"<{}:{}>".format(
|
||||||
TYPE_TO_GENERATOR_MAP.get("str")(),
|
TYPE_TO_GENERATOR_MAP.get("string")(),
|
||||||
random.choice(self.ROUTE_PARAM_TYPES),
|
random.choice(self.ROUTE_PARAM_TYPES),
|
||||||
)
|
)
|
||||||
for _ in range(max_route_depth - current_length)
|
for _ in range(max_route_depth - current_length)
|
||||||
@@ -96,7 +97,7 @@ class RouteStringGenerator:
|
|||||||
def generate_url_for_template(template):
|
def generate_url_for_template(template):
|
||||||
url = template
|
url = template
|
||||||
for pattern, param_type in re.findall(
|
for pattern, param_type in re.findall(
|
||||||
re.compile(r"((?:<\w+:(str|int|float|alpha|uuid)>)+)"),
|
re.compile(r"((?:<\w+:(string|int|number|alpha|uuid)>)+)"),
|
||||||
template,
|
template,
|
||||||
):
|
):
|
||||||
value = TYPE_TO_GENERATOR_MAP.get(param_type)()
|
value = TYPE_TO_GENERATOR_MAP.get(param_type)()
|
||||||
@@ -109,7 +110,6 @@ def sanic_router(app):
|
|||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
def _setup(route_details: tuple) -> Tuple[Router, tuple]:
|
def _setup(route_details: tuple) -> Tuple[Router, tuple]:
|
||||||
router = Router()
|
router = Router()
|
||||||
router.ctx.app = app
|
|
||||||
added_router = []
|
added_router = []
|
||||||
for method, route in route_details:
|
for method, route in route_details:
|
||||||
try:
|
try:
|
||||||
@@ -140,33 +140,5 @@ def url_param_generator():
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def app(request):
|
def app(request):
|
||||||
if not CACHE:
|
app = Sanic(request.node.name)
|
||||||
for target, method_name in TouchUp._registry:
|
return app
|
||||||
CACHE[method_name] = getattr(target, method_name)
|
|
||||||
app = Sanic(slugify.sub("-", request.node.name))
|
|
||||||
yield app
|
|
||||||
for target, method_name in TouchUp._registry:
|
|
||||||
setattr(target, method_name, CACHE[method_name])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def run_startup(caplog):
|
|
||||||
def run(app):
|
|
||||||
nonlocal caplog
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
with caplog.at_level(logging.DEBUG):
|
|
||||||
server = app.create_server(
|
|
||||||
debug=True, return_asyncio_server=True, port=PORT
|
|
||||||
)
|
|
||||||
loop._stopping = False
|
|
||||||
|
|
||||||
_server = loop.run_until_complete(server)
|
|
||||||
|
|
||||||
_server.close()
|
|
||||||
loop.run_until_complete(_server.wait_closed())
|
|
||||||
app.stop()
|
|
||||||
|
|
||||||
return caplog.record_tuples
|
|
||||||
|
|
||||||
return run
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from sanic import Sanic, text
|
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
|
||||||
|
|
||||||
|
|
||||||
LOGGING_CONFIG = {**LOGGING_CONFIG_DEFAULTS}
|
|
||||||
LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s"
|
|
||||||
LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG"
|
|
||||||
|
|
||||||
app = Sanic(__name__, log_config=LOGGING_CONFIG)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def handler(request):
|
|
||||||
return text(request.ip)
|
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_start
|
|
||||||
async def app_info_dump(app: Sanic, _):
|
|
||||||
app_data = {
|
|
||||||
"access_log": app.config.ACCESS_LOG,
|
|
||||||
"auto_reload": app.auto_reload,
|
|
||||||
"debug": app.debug,
|
|
||||||
}
|
|
||||||
logger.info(json.dumps(app_data))
|
|
||||||
|
|
||||||
|
|
||||||
@app.after_server_start
|
|
||||||
async def shutdown(app: Sanic, _):
|
|
||||||
app.stop()
|
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
|
||||||
return app
|
|
||||||
@@ -9,7 +9,6 @@ from unittest.mock import Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.config import Config
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@@ -178,6 +177,9 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
|
|||||||
@patch("sanic.app.WebSocketProtocol")
|
@patch("sanic.app.WebSocketProtocol")
|
||||||
def test_app_websocket_parameters(websocket_protocol_mock, app):
|
def test_app_websocket_parameters(websocket_protocol_mock, app):
|
||||||
app.config.WEBSOCKET_MAX_SIZE = 44
|
app.config.WEBSOCKET_MAX_SIZE = 44
|
||||||
|
app.config.WEBSOCKET_MAX_QUEUE = 45
|
||||||
|
app.config.WEBSOCKET_READ_LIMIT = 46
|
||||||
|
app.config.WEBSOCKET_WRITE_LIMIT = 47
|
||||||
app.config.WEBSOCKET_PING_TIMEOUT = 48
|
app.config.WEBSOCKET_PING_TIMEOUT = 48
|
||||||
app.config.WEBSOCKET_PING_INTERVAL = 50
|
app.config.WEBSOCKET_PING_INTERVAL = 50
|
||||||
|
|
||||||
@@ -194,6 +196,11 @@ def test_app_websocket_parameters(websocket_protocol_mock, app):
|
|||||||
websocket_protocol_call_args = websocket_protocol_mock.call_args
|
websocket_protocol_call_args = websocket_protocol_mock.call_args
|
||||||
ws_kwargs = websocket_protocol_call_args[1]
|
ws_kwargs = websocket_protocol_call_args[1]
|
||||||
assert ws_kwargs["websocket_max_size"] == app.config.WEBSOCKET_MAX_SIZE
|
assert ws_kwargs["websocket_max_size"] == app.config.WEBSOCKET_MAX_SIZE
|
||||||
|
assert ws_kwargs["websocket_max_queue"] == app.config.WEBSOCKET_MAX_QUEUE
|
||||||
|
assert ws_kwargs["websocket_read_limit"] == app.config.WEBSOCKET_READ_LIMIT
|
||||||
|
assert (
|
||||||
|
ws_kwargs["websocket_write_limit"] == app.config.WEBSOCKET_WRITE_LIMIT
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
ws_kwargs["websocket_ping_timeout"]
|
ws_kwargs["websocket_ping_timeout"]
|
||||||
== app.config.WEBSOCKET_PING_TIMEOUT
|
== app.config.WEBSOCKET_PING_TIMEOUT
|
||||||
@@ -269,7 +276,7 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
|
|||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
assert "Mock SanicException" in response.text
|
assert "Mock SanicException" in response.text
|
||||||
assert (
|
assert (
|
||||||
"sanic.error",
|
"sanic.root",
|
||||||
logging.ERROR,
|
logging.ERROR,
|
||||||
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
|
f"Exception occurred while handling uri: 'http://127.0.0.1:{port}/'",
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
@@ -382,13 +389,13 @@ def test_app_no_registry_env():
|
|||||||
|
|
||||||
|
|
||||||
def test_app_set_attribute_warning(app):
|
def test_app_set_attribute_warning(app):
|
||||||
with pytest.warns(DeprecationWarning) as record:
|
with pytest.warns(UserWarning) as record:
|
||||||
app.foo = 1
|
app.foo = 1
|
||||||
|
|
||||||
assert len(record) == 1
|
assert len(record) == 1
|
||||||
assert record[0].message.args[0] == (
|
assert record[0].message.args[0] == (
|
||||||
"Setting variables on Sanic instances is deprecated "
|
"Setting variables on Sanic instances is deprecated "
|
||||||
"and will be removed in version 21.12. You should change your "
|
"and will be removed in version 21.9. You should change your "
|
||||||
"Sanic instance to use instance.ctx.foo instead."
|
"Sanic instance to use instance.ctx.foo instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -405,42 +412,3 @@ def test_subclass_initialisation():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
CustomSanic("test_subclass_initialisation")
|
CustomSanic("test_subclass_initialisation")
|
||||||
|
|
||||||
|
|
||||||
def test_bad_custom_config():
|
|
||||||
with pytest.raises(
|
|
||||||
SanicException,
|
|
||||||
match=(
|
|
||||||
"When instantiating Sanic with config, you cannot also pass "
|
|
||||||
"load_env or env_prefix"
|
|
||||||
),
|
|
||||||
):
|
|
||||||
Sanic("test", config=1, load_env=1)
|
|
||||||
with pytest.raises(
|
|
||||||
SanicException,
|
|
||||||
match=(
|
|
||||||
"When instantiating Sanic with config, you cannot also pass "
|
|
||||||
"load_env or env_prefix"
|
|
||||||
),
|
|
||||||
):
|
|
||||||
Sanic("test", config=1, env_prefix=1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_config():
|
|
||||||
class CustomConfig(Config):
|
|
||||||
...
|
|
||||||
|
|
||||||
config = CustomConfig()
|
|
||||||
app = Sanic("custom", config=config)
|
|
||||||
|
|
||||||
assert app.config == config
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_context():
|
|
||||||
class CustomContext:
|
|
||||||
...
|
|
||||||
|
|
||||||
ctx = CustomContext()
|
|
||||||
app = Sanic("custom", ctx=ctx)
|
|
||||||
|
|
||||||
assert app.ctx == ctx
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
from collections import deque, namedtuple
|
from collections import deque, namedtuple
|
||||||
|
|
||||||
@@ -7,10 +8,10 @@ import uvicorn
|
|||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.asgi import MockTransport
|
from sanic.asgi import MockTransport
|
||||||
from sanic.exceptions import Forbidden, InvalidUsage, ServiceUnavailable
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.server.websockets.connection import WebSocketConnection
|
from sanic.websocket import WebSocketConnection
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -218,7 +219,7 @@ async def test_websocket_accept_with_no_subprotocols(
|
|||||||
|
|
||||||
message = message_stack.popleft()
|
message = message_stack.popleft()
|
||||||
assert message["type"] == "websocket.accept"
|
assert message["type"] == "websocket.accept"
|
||||||
assert message["subprotocol"] is None
|
assert message["subprotocol"] == ""
|
||||||
assert "bytes" not in message
|
assert "bytes" not in message
|
||||||
|
|
||||||
|
|
||||||
@@ -227,7 +228,7 @@ async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
|
|||||||
subprotocols = ["graphql-ws"]
|
subprotocols = ["graphql-ws"]
|
||||||
|
|
||||||
ws = WebSocketConnection(send, receive, subprotocols)
|
ws = WebSocketConnection(send, receive, subprotocols)
|
||||||
await ws.accept(subprotocols)
|
await ws.accept()
|
||||||
|
|
||||||
assert len(message_stack) == 1
|
assert len(message_stack) == 1
|
||||||
|
|
||||||
@@ -244,13 +245,13 @@ async def test_websocket_accept_with_multiple_subprotocols(
|
|||||||
subprotocols = ["graphql-ws", "hello", "world"]
|
subprotocols = ["graphql-ws", "hello", "world"]
|
||||||
|
|
||||||
ws = WebSocketConnection(send, receive, subprotocols)
|
ws = WebSocketConnection(send, receive, subprotocols)
|
||||||
await ws.accept(["hello", "world"])
|
await ws.accept()
|
||||||
|
|
||||||
assert len(message_stack) == 1
|
assert len(message_stack) == 1
|
||||||
|
|
||||||
message = message_stack.popleft()
|
message = message_stack.popleft()
|
||||||
assert message["type"] == "websocket.accept"
|
assert message["type"] == "websocket.accept"
|
||||||
assert message["subprotocol"] == "hello"
|
assert message["subprotocol"] == "graphql-ws,hello,world"
|
||||||
assert "bytes" not in message
|
assert "bytes" not in message
|
||||||
|
|
||||||
|
|
||||||
@@ -346,33 +347,3 @@ async def test_content_type(app):
|
|||||||
|
|
||||||
_, response = await app.asgi_client.get("/custom")
|
_, response = await app.asgi_client.get("/custom")
|
||||||
assert response.headers.get("content-type") == "somethingelse"
|
assert response.headers.get("content-type") == "somethingelse"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_request_handle_exception(app):
|
|
||||||
@app.get("/error-prone")
|
|
||||||
def _request(request):
|
|
||||||
raise ServiceUnavailable(message="Service unavailable")
|
|
||||||
|
|
||||||
_, response = await app.asgi_client.get("/wrong-path")
|
|
||||||
assert response.status_code == 404
|
|
||||||
|
|
||||||
_, response = await app.asgi_client.get("/error-prone")
|
|
||||||
assert response.status_code == 503
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_request_exception_suppressed_by_middleware(app):
|
|
||||||
@app.get("/error-prone")
|
|
||||||
def _request(request):
|
|
||||||
raise ServiceUnavailable(message="Service unavailable")
|
|
||||||
|
|
||||||
@app.on_request
|
|
||||||
def forbidden(request):
|
|
||||||
raise Forbidden(message="forbidden")
|
|
||||||
|
|
||||||
_, response = await app.asgi_client.get("/wrong-path")
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
_, response = await app.asgi_client.get("/error-prone")
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user