Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8673021ad4 |
@@ -1,7 +1,5 @@
|
|||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
- "sanic/__main__.py"
|
- "sanic/__main__.py"
|
||||||
- "sanic/application/logo.py"
|
|
||||||
- "sanic/application/motd.py"
|
|
||||||
- "sanic/reloader_helpers.py"
|
- "sanic/reloader_helpers.py"
|
||||||
- "sanic/simple.py"
|
- "sanic/simple.py"
|
||||||
- "sanic/utils.py"
|
- "sanic/utils.py"
|
||||||
@@ -10,6 +8,7 @@ exclude_patterns:
|
|||||||
- "docker/"
|
- "docker/"
|
||||||
- "docs/"
|
- "docs/"
|
||||||
- "examples/"
|
- "examples/"
|
||||||
|
- "hack/"
|
||||||
- "scripts/"
|
- "scripts/"
|
||||||
- "tests/"
|
- "tests/"
|
||||||
checks:
|
checks:
|
||||||
@@ -23,6 +22,3 @@ checks:
|
|||||||
threshold: 40
|
threshold: 40
|
||||||
complex-logic:
|
complex-logic:
|
||||||
enabled: false
|
enabled: false
|
||||||
method-complexity:
|
|
||||||
config:
|
|
||||||
threshold: 10
|
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ branch = True
|
|||||||
source = sanic
|
source = sanic
|
||||||
omit =
|
omit =
|
||||||
site-packages
|
site-packages
|
||||||
sanic/application/logo.py
|
|
||||||
sanic/application/motd.py
|
|
||||||
sanic/cli
|
|
||||||
sanic/__main__.py
|
sanic/__main__.py
|
||||||
sanic/reloader_helpers.py
|
sanic/reloader_helpers.py
|
||||||
sanic/simple.py
|
sanic/simple.py
|
||||||
|
|||||||
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
@@ -2,20 +2,14 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [ main ]
|
||||||
- main
|
|
||||||
- "*LTS"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches: [ main ]
|
||||||
- main
|
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '25 16 * * 0'
|
- cron: '25 16 * * 0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
23
.github/workflows/coverage.yml
vendored
23
.github/workflows/coverage.yml
vendored
@@ -1,16 +1,19 @@
|
|||||||
name: Coverage check
|
name: Coverage check
|
||||||
on:
|
# on:
|
||||||
push:
|
# push:
|
||||||
branches:
|
# branches:
|
||||||
- main
|
# - main
|
||||||
- "*LTS"
|
# tags:
|
||||||
tags:
|
# - "!*" # Do not execute on tags
|
||||||
- "!*" # Do not execute on tags
|
# paths:
|
||||||
pull_request:
|
# - sanic/*
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
# - tests/*
|
||||||
|
# pull_request:
|
||||||
|
# paths:
|
||||||
|
# - "!*.MD"
|
||||||
|
on: [push, pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
4
.github/workflows/pr-bandit.yml
vendored
4
.github/workflows/pr-bandit.yml
vendored
@@ -3,12 +3,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bandit:
|
bandit:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: type-check-${{ matrix.config.python-version }}
|
name: type-check-${{ matrix.config.python-version }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -19,7 +16,6 @@ jobs:
|
|||||||
- { python-version: 3.7, tox-env: security}
|
- { python-version: 3.7, tox-env: security}
|
||||||
- { python-version: 3.8, tox-env: security}
|
- { python-version: 3.8, tox-env: security}
|
||||||
- { python-version: 3.9, tox-env: security}
|
- { python-version: 3.9, tox-env: security}
|
||||||
- { python-version: "3.10", tox-env: security}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
3
.github/workflows/pr-docs.yml
vendored
3
.github/workflows/pr-docs.yml
vendored
@@ -3,12 +3,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docsLinter:
|
docsLinter:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: Lint Documentation
|
name: Lint Documentation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
3
.github/workflows/pr-linter.yml
vendored
3
.github/workflows/pr-linter.yml
vendored
@@ -3,12 +3,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linter:
|
linter:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: lint
|
name: lint
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
47
.github/workflows/pr-python310.yml
vendored
47
.github/workflows/pr-python310.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Python 3.10 Tests
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testPy310:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# os: [ubuntu-latest, macos-latest]
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
config:
|
|
||||||
- {
|
|
||||||
python-version: "3.10",
|
|
||||||
tox-env: py310,
|
|
||||||
ignore-error-flake: "false",
|
|
||||||
command-timeout: "0",
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
python-version: "3.10",
|
|
||||||
tox-env: py310-no-ext,
|
|
||||||
ignore-error-flake: "true",
|
|
||||||
command-timeout: "600000",
|
|
||||||
}
|
|
||||||
steps:
|
|
||||||
- name: Checkout the Repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
id: checkout-branch
|
|
||||||
|
|
||||||
- name: Run Unit Tests
|
|
||||||
uses: harshanarayana/custom-actions@main
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.config.python-version }}
|
|
||||||
test-infra-tool: tox
|
|
||||||
test-infra-version: latest
|
|
||||||
action: tests
|
|
||||||
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
|
|
||||||
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
|
|
||||||
command-timeout: "${{ matrix.config.command-timeout }}"
|
|
||||||
test-failure-retry: "3"
|
|
||||||
11
.github/workflows/pr-python37.yml
vendored
11
.github/workflows/pr-python37.yml
vendored
@@ -3,16 +3,19 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
push:
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- sanic/*
|
||||||
|
- tests/*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
testPy37:
|
testPy37:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# os: [ubuntu-latest, macos-latest]
|
# os: [ubuntu-latest, macos-latest]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|||||||
11
.github/workflows/pr-python38.yml
vendored
11
.github/workflows/pr-python38.yml
vendored
@@ -3,16 +3,19 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
push:
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- sanic/*
|
||||||
|
- tests/*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
testPy38:
|
testPy38:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# os: [ubuntu-latest, macos-latest]
|
# os: [ubuntu-latest, macos-latest]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|||||||
11
.github/workflows/pr-python39.yml
vendored
11
.github/workflows/pr-python39.yml
vendored
@@ -3,16 +3,19 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
push:
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- sanic/*
|
||||||
|
- tests/*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
testPy39:
|
testPy39:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# os: [ubuntu-latest, macos-latest]
|
# os: [ubuntu-latest, macos-latest]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|||||||
6
.github/workflows/pr-type-check.yml
vendored
6
.github/workflows/pr-type-check.yml
vendored
@@ -3,12 +3,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
typeChecking:
|
typeChecking:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: type-check-${{ matrix.config.python-version }}
|
name: type-check-${{ matrix.config.python-version }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -16,10 +13,9 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
config:
|
config:
|
||||||
# - { python-version: 3.7, tox-env: type-checking}
|
- { python-version: 3.7, tox-env: type-checking}
|
||||||
- { python-version: 3.8, tox-env: type-checking}
|
- { python-version: 3.8, tox-env: type-checking}
|
||||||
- { python-version: 3.9, tox-env: type-checking}
|
- { python-version: 3.9, tox-env: type-checking}
|
||||||
- { python-version: "3.10", tox-env: type-checking}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
4
.github/workflows/pr-windows.yml
vendored
4
.github/workflows/pr-windows.yml
vendored
@@ -3,12 +3,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "*LTS"
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
testsOnWindows:
|
testsOnWindows:
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: ut-${{ matrix.config.tox-env }}
|
name: ut-${{ matrix.config.tox-env }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
@@ -18,7 +15,6 @@ jobs:
|
|||||||
- { python-version: 3.7, tox-env: py37-no-ext }
|
- { python-version: 3.7, tox-env: py37-no-ext }
|
||||||
- { python-version: 3.8, tox-env: py38-no-ext }
|
- { python-version: 3.8, tox-env: py38-no-ext }
|
||||||
- { python-version: 3.9, tox-env: py39-no-ext }
|
- { python-version: 3.9, tox-env: py39-no-ext }
|
||||||
- { python-version: "3.10", tox-env: py310-no-ext }
|
|
||||||
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
|
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
2
.github/workflows/publish-images.yml
vendored
2
.github/workflows/publish-images.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
python-version: ["3.7", "3.8", "3.9"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
198
CHANGELOG.rst
198
CHANGELOG.rst
@@ -1,12 +1,12 @@
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html.
|
From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases``
|
||||||
|
|
||||||
|
|
||||||
Version 21.6.1
|
Version 21.6.1
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
|
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
|
||||||
Update sanic-routing to allow for better splitting of complex URI templates
|
Update sanic-routing to allow for better splitting of complex URI templates
|
||||||
@@ -20,7 +20,8 @@ Version 21.6.1
|
|||||||
Version 21.6.0
|
Version 21.6.0
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
|
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
|
||||||
Add ``response.eof()`` method for closing a stream in a handler
|
Add ``response.eof()`` method for closing a stream in a handler
|
||||||
@@ -67,7 +68,8 @@ Version 21.6.0
|
|||||||
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
|
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||||
Additional methods for attaching ``HTTPMethodView``
|
Additional methods for attaching ``HTTPMethodView``
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
|
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
|
||||||
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
|
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
|
||||||
@@ -83,7 +85,8 @@ Version 21.6.0
|
|||||||
Fix issue where Blueprint exception handlers do not consistently route to proper handler
|
Fix issue where Blueprint exception handlers do not consistently route to proper handler
|
||||||
|
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
|
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
|
||||||
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
|
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
|
||||||
@@ -92,12 +95,14 @@ Version 21.6.0
|
|||||||
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
|
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
|
||||||
Deprecate StreamingHTTPResponse
|
Deprecate StreamingHTTPResponse
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
|
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
|
||||||
Remove Travis CI in favor of GitHub Actions
|
Remove Travis CI in favor of GitHub Actions
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
|
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
|
||||||
Fix typo in documentation
|
Fix typo in documentation
|
||||||
@@ -107,7 +112,8 @@ Version 21.6.0
|
|||||||
Version 21.3.2
|
Version 21.3.2
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
|
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
|
||||||
Disable response timeout on websocket connections
|
Disable response timeout on websocket connections
|
||||||
@@ -118,7 +124,8 @@ Version 21.3.2
|
|||||||
Version 21.3.1
|
Version 21.3.1
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
|
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
|
||||||
Static files inside subfolders are not accessible (404)
|
Static files inside subfolders are not accessible (404)
|
||||||
@@ -128,7 +135,8 @@ Version 21.3.0
|
|||||||
|
|
||||||
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
|
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
|
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
|
||||||
@@ -181,7 +189,8 @@ Version 21.3.0
|
|||||||
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
|
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
|
||||||
App and connection level context objects
|
App and connection level context objects
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes and issues resolved
|
||||||
|
****************************
|
||||||
|
|
||||||
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
|
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
|
||||||
``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
|
``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
|
||||||
@@ -211,7 +220,8 @@ Version 21.3.0
|
|||||||
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
|
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
|
||||||
Raise ValueError when cookie max-age is not an integer
|
Raise ValueError when cookie max-age is not an integer
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
|
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
|
||||||
@@ -230,7 +240,8 @@ Version 21.3.0
|
|||||||
* ``Request.endpoint`` deprecated in favor of ``Request.name``
|
* ``Request.endpoint`` deprecated in favor of ``Request.name``
|
||||||
* handler type name prefixes removed (static, websocket, etc)
|
* handler type name prefixes removed (static, websocket, etc)
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
|
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
|
||||||
@@ -248,7 +259,8 @@ Version 21.3.0
|
|||||||
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
|
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
|
||||||
Updated setup.py to use ``find_packages``
|
Updated setup.py to use ``find_packages``
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
|
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
|
||||||
@@ -270,7 +282,8 @@ Version 21.3.0
|
|||||||
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
|
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
|
||||||
Fix some examples and docs
|
Fix some examples and docs
|
||||||
|
|
||||||
**Miscellaneous**
|
Miscellaneous
|
||||||
|
*************
|
||||||
|
|
||||||
* ``Request.route`` property
|
* ``Request.route`` property
|
||||||
* Better websocket subprotocols support
|
* Better websocket subprotocols support
|
||||||
@@ -316,7 +329,8 @@ Version 21.3.0
|
|||||||
Version 20.12.3
|
Version 20.12.3
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
|
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
|
||||||
@@ -325,7 +339,8 @@ Version 20.12.3
|
|||||||
Version 20.12.2
|
Version 20.12.2
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Dependencies**
|
Dependencies
|
||||||
|
************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
|
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
|
||||||
@@ -338,7 +353,8 @@ Version 20.12.2
|
|||||||
Version 19.12.5
|
Version 19.12.5
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Dependencies**
|
Dependencies
|
||||||
|
************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
|
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
|
||||||
@@ -351,7 +367,8 @@ Version 19.12.5
|
|||||||
Version 20.12.0
|
Version 20.12.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
|
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
|
||||||
@@ -360,7 +377,8 @@ Version 20.12.0
|
|||||||
Version 20.12.0
|
Version 20.12.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
|
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
|
||||||
@@ -398,19 +416,22 @@ Version 20.12.0
|
|||||||
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
|
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
|
||||||
Add app registry and Sanic class level app retrieval
|
Add app registry and Sanic class level app retrieval
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
|
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
|
||||||
Fix Chunked Transport-Encoding in ASGI streaming response
|
Fix Chunked Transport-Encoding in ASGI streaming response
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
|
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
|
||||||
Cleanup and remove deprecated code
|
Cleanup and remove deprecated code
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
|
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
|
||||||
@@ -424,7 +445,8 @@ Version 20.12.0
|
|||||||
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
|
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
|
||||||
Update tox requirements
|
Update tox requirements
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
|
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
|
||||||
@@ -442,7 +464,8 @@ Version 20.12.0
|
|||||||
Version 20.9.1
|
Version 20.9.1
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
|
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
|
||||||
@@ -455,7 +478,8 @@ Version 20.9.1
|
|||||||
Version 19.12.3
|
Version 19.12.3
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
|
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
|
||||||
@@ -466,7 +490,8 @@ Version 20.9.0
|
|||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
|
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
|
||||||
@@ -493,19 +518,22 @@ Version 20.9.0
|
|||||||
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
|
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
|
||||||
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
|
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
|
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
|
||||||
Resolves exception from unread bytes in stream
|
Resolves exception from unread bytes in stream
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
|
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
|
||||||
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
|
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
|
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
|
||||||
@@ -520,7 +548,8 @@ Version 20.9.0
|
|||||||
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
|
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
|
||||||
Adding --strict-markers for pytest
|
Adding --strict-markers for pytest
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
|
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
|
||||||
@@ -530,7 +559,8 @@ Version 20.9.0
|
|||||||
Version 20.6.3
|
Version 20.6.3
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
|
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
|
||||||
@@ -540,7 +570,8 @@ Version 20.6.3
|
|||||||
Version 20.6.2
|
Version 20.6.2
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
|
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
|
||||||
@@ -550,7 +581,8 @@ Version 20.6.2
|
|||||||
Version 20.6.1
|
Version 20.6.1
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
|
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
|
||||||
@@ -564,7 +596,8 @@ Version 20.6.1
|
|||||||
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
|
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
|
||||||
Add handler names for websockets for url_for usage
|
Add handler names for websockets for url_for usage
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
|
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
|
||||||
@@ -586,13 +619,15 @@ Version 20.6.1
|
|||||||
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
|
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
|
||||||
Fix pickle error when attempting to pickle an application which contains websocket routes
|
Fix pickle error when attempting to pickle an application which contains websocket routes
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
|
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
|
||||||
Deprecate body_bytes to merge into body
|
Deprecate body_bytes to merge into body
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
|
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
|
||||||
@@ -607,7 +642,8 @@ Version 20.6.1
|
|||||||
Wrap run()'s "protocol" type annotation in Optional[]
|
Wrap run()'s "protocol" type annotation in Optional[]
|
||||||
|
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
|
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
|
||||||
@@ -621,13 +657,14 @@ Version 20.6.1
|
|||||||
Version 20.6.0
|
Version 20.6.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
*Released, but unintentionally omitting PR #1880, so was replaced by 20.6.1*
|
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
|
||||||
|
|
||||||
|
|
||||||
Version 20.3.0
|
Version 20.3.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
|
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
|
||||||
@@ -658,7 +695,8 @@ Version 20.3.0
|
|||||||
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
|
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
|
||||||
Do not set content-type and content-length headers in exceptions
|
Do not set content-type and content-length headers in exceptions
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
|
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
|
||||||
@@ -676,7 +714,8 @@ Version 20.3.0
|
|||||||
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
|
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
|
||||||
Fix Ctrl+C and tests on Windows
|
Fix Ctrl+C and tests on Windows
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
|
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
|
||||||
@@ -694,7 +733,8 @@ Version 20.3.0
|
|||||||
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
|
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
|
||||||
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
|
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
|
||||||
|
|
||||||
**Dependencies**
|
Dependencies
|
||||||
|
************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
|
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
|
||||||
@@ -704,13 +744,15 @@ Version 20.3.0
|
|||||||
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
|
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
|
||||||
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
|
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
|
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
|
||||||
Resolve broken documentation builds
|
Resolve broken documentation builds
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
|
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
|
||||||
@@ -752,7 +794,8 @@ Version 20.3.0
|
|||||||
Version 19.12.0
|
Version 19.12.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
- Fix blueprint middleware application
|
- Fix blueprint middleware application
|
||||||
|
|
||||||
@@ -771,7 +814,8 @@ Version 19.12.0
|
|||||||
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
|
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
|
||||||
|
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
- Move docs from MD to RST
|
- Move docs from MD to RST
|
||||||
|
|
||||||
@@ -785,7 +829,8 @@ Version 19.12.0
|
|||||||
Version 19.6.3
|
Version 19.6.3
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
- Enable Towncrier Support
|
- Enable Towncrier Support
|
||||||
|
|
||||||
@@ -793,7 +838,8 @@ Version 19.6.3
|
|||||||
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
|
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
|
||||||
|
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
- Documentation infrastructure changes
|
- Documentation infrastructure changes
|
||||||
|
|
||||||
@@ -806,7 +852,8 @@ Version 19.6.3
|
|||||||
Version 19.6.2
|
Version 19.6.2
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
|
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
|
||||||
@@ -822,7 +869,8 @@ Version 19.6.2
|
|||||||
Add Configure support from object string
|
Add Configure support from object string
|
||||||
|
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
|
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
|
||||||
@@ -840,7 +888,8 @@ Version 19.6.2
|
|||||||
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
|
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
|
||||||
Strict Slashes behavior fix
|
Strict Slashes behavior fix
|
||||||
|
|
||||||
**Deprecations and Removals**
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
|
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
|
||||||
@@ -864,7 +913,8 @@ Version 19.6.2
|
|||||||
Version 19.3
|
Version 19.3
|
||||||
------------
|
------------
|
||||||
|
|
||||||
**Features**
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
*
|
*
|
||||||
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
|
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
|
||||||
@@ -932,7 +982,8 @@ Version 19.3
|
|||||||
|
|
||||||
This is a breaking change.
|
This is a breaking change.
|
||||||
|
|
||||||
**Bugfixes**
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
|
||||||
*
|
*
|
||||||
@@ -968,7 +1019,8 @@ Version 19.3
|
|||||||
This allows the access log to be disabled for example when running via
|
This allows the access log to be disabled for example when running via
|
||||||
gunicorn.
|
gunicorn.
|
||||||
|
|
||||||
**Developer infrastructure**
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
|
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
|
||||||
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
|
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
|
||||||
@@ -976,7 +1028,8 @@ Version 19.3
|
|||||||
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
|
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
|
||||||
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
|
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
|
||||||
|
|
||||||
**Improved Documentation**
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
|
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
|
||||||
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
|
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
|
||||||
@@ -1037,19 +1090,21 @@ Version 18.12
|
|||||||
* Fix Range header handling for static files (#1402)
|
* Fix Range header handling for static files (#1402)
|
||||||
* Fix the logger and make it work (#1397)
|
* Fix the logger and make it work (#1397)
|
||||||
* Fix type pikcle->pickle in multiprocessing test
|
* Fix type pikcle->pickle in multiprocessing test
|
||||||
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirement of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||||
* Fix document for logging
|
* Fix document for logging
|
||||||
|
|
||||||
Version 0.8
|
Version 0.8
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**0.8.3**
|
0.8.3
|
||||||
|
*****
|
||||||
|
|
||||||
* Changes:
|
* Changes:
|
||||||
|
|
||||||
* Ownership changed to org 'sanic-org'
|
* Ownership changed to org 'sanic-org'
|
||||||
|
|
||||||
**0.8.0**
|
0.8.0
|
||||||
|
*****
|
||||||
|
|
||||||
* Changes:
|
* Changes:
|
||||||
|
|
||||||
@@ -1074,7 +1129,7 @@ Version 0.8
|
|||||||
* Content-length header on 204/304 responses (Arnulfo Solís)
|
* Content-length header on 204/304 responses (Arnulfo Solís)
|
||||||
* Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
* Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
||||||
* Update development status from pre-alpha to beta (Maksim Anisenkov)
|
* Update development status from pre-alpha to beta (Maksim Anisenkov)
|
||||||
* KeepAlive Timeout log level changed to debug (Arnulfo Solís)
|
* KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
||||||
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
||||||
* Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
* Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
||||||
* Add support for blueprint groups and nesting (Elias Tarhini)
|
* Add support for blueprint groups and nesting (Elias Tarhini)
|
||||||
@@ -1129,16 +1184,19 @@ Version 0.1
|
|||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
||||||
**0.1.7**
|
0.1.7
|
||||||
|
*****
|
||||||
|
|
||||||
* Reversed static url and directory arguments to meet spec
|
* Reversed static url and directory arguments to meet spec
|
||||||
|
|
||||||
**0.1.6**
|
0.1.6
|
||||||
|
*****
|
||||||
|
|
||||||
* Static files
|
* Static files
|
||||||
* Lazy Cookie Loading
|
* Lazy Cookie Loading
|
||||||
|
|
||||||
**0.1.5**
|
0.1.5
|
||||||
|
*****
|
||||||
|
|
||||||
* Cookies
|
* Cookies
|
||||||
* Blueprint listeners and ordering
|
* Blueprint listeners and ordering
|
||||||
@@ -1146,19 +1204,23 @@ Version 0.1
|
|||||||
* Fix: Incomplete file reads on medium+ sized post requests
|
* Fix: Incomplete file reads on medium+ sized post requests
|
||||||
* Breaking: after_start and before_stop now pass sanic as their first argument
|
* Breaking: after_start and before_stop now pass sanic as their first argument
|
||||||
|
|
||||||
**0.1.4**
|
0.1.4
|
||||||
|
*****
|
||||||
|
|
||||||
* Multiprocessing
|
* Multiprocessing
|
||||||
|
|
||||||
**0.1.3**
|
0.1.3
|
||||||
|
*****
|
||||||
|
|
||||||
* Blueprint support
|
* Blueprint support
|
||||||
* Faster Response processing
|
* Faster Response processing
|
||||||
|
|
||||||
**0.1.1 - 0.1.2**
|
0.1.1 - 0.1.2
|
||||||
|
*************
|
||||||
|
|
||||||
* Struggling to update pypi via CI
|
* Struggling to update pypi via CI
|
||||||
|
|
||||||
**0.1.0**
|
0.1.0
|
||||||
|
*****
|
||||||
|
|
||||||
* Released to public
|
* Released to public
|
||||||
|
|||||||
17
README.rst
17
README.rst
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
|||||||
:stub-columns: 1
|
:stub-columns: 1
|
||||||
|
|
||||||
* - Build
|
* - Build
|
||||||
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
|
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
|
||||||
* - Docs
|
* - Docs
|
||||||
- | |UserGuide| |Documentation|
|
- | |UserGuide| |Documentation|
|
||||||
* - Package
|
* - Package
|
||||||
@@ -27,8 +27,8 @@ Sanic | Build fast. Run fast.
|
|||||||
:target: https://community.sanicframework.org/
|
:target: https://community.sanicframework.org/
|
||||||
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
|
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
|
||||||
:target: https://discord.gg/FARQzAEMAA
|
:target: https://discord.gg/FARQzAEMAA
|
||||||
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
|
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
||||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
|
: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
|
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
|
||||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
|
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
|
||||||
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
|
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
|
||||||
@@ -77,11 +77,7 @@ 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.
|
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|
||||||
|
|
||||||
Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic.
|
|
||||||
|
|
||||||
|Linode|
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
@@ -166,8 +162,3 @@ Contribution
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
|
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
|
||||||
|
|
||||||
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
|
|
||||||
:alt: Linode
|
|
||||||
:target: https://www.linode.com
|
|
||||||
:width: 200px
|
|
||||||
|
|||||||
@@ -38,3 +38,10 @@ sanic.views
|
|||||||
.. automodule:: sanic.views
|
.. automodule:: sanic.views
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.websocket
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.websocket
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
📜 Changelog
|
📜 Changelog
|
||||||
============
|
============
|
||||||
|
|
||||||
.. mdinclude:: ./releases/21/21.12.md
|
.. mdinclude:: ./releases/21.9.md
|
||||||
.. mdinclude:: ./releases/21/21.9.md
|
|
||||||
.. include:: ../../CHANGELOG.rst
|
.. include:: ../../CHANGELOG.rst
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
## Version 21.9.3
|
## Version 21.9
|
||||||
*Rerelease of v21.9.2 with some cleanup*
|
|
||||||
|
|
||||||
## Version 21.9.2
|
|
||||||
- [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages
|
|
||||||
- [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply
|
|
||||||
|
|
||||||
## Version 21.9.1
|
|
||||||
- [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers
|
|
||||||
|
|
||||||
## Version 21.9.0
|
|
||||||
|
|
||||||
### Features
|
### 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
|
- [#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
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
## Version 21.12.1
|
|
||||||
|
|
||||||
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
|
|
||||||
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
|
|
||||||
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
|
|
||||||
|
|
||||||
## Version 21.12.0
|
|
||||||
|
|
||||||
### Features
|
|
||||||
- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects
|
|
||||||
- [#2262](https://github.com/sanic-org/sanic/pull/2262) Noisy exceptions - force logging of all exceptions
|
|
||||||
- [#2264](https://github.com/sanic-org/sanic/pull/2264) Optional `uvloop` by configuration
|
|
||||||
- [#2270](https://github.com/sanic-org/sanic/pull/2270) Vhost support using multiple TLS certificates
|
|
||||||
- [#2277](https://github.com/sanic-org/sanic/pull/2277) Change signal routing for increased consistency
|
|
||||||
- *BREAKING CHANGE*: If you were manually routing signals there is a breaking change. The signal router's `get` is no longer 100% determinative. There is now an additional step to loop thru the returned signals for proper matching on the requirements. If signals are being dispatched using `app.dispatch` or `bp.dispatch`, there is no change.
|
|
||||||
- [#2290](https://github.com/sanic-org/sanic/pull/2290) Add contextual exceptions
|
|
||||||
- [#2291](https://github.com/sanic-org/sanic/pull/2291) Increase join concat performance
|
|
||||||
- [#2295](https://github.com/sanic-org/sanic/pull/2295), [#2316](https://github.com/sanic-org/sanic/pull/2316), [#2331](https://github.com/sanic-org/sanic/pull/2331) Restructure of CLI and application state with new displays and more command parity with `app.run`
|
|
||||||
- [#2302](https://github.com/sanic-org/sanic/pull/2302) Add route context at definition time
|
|
||||||
- [#2304](https://github.com/sanic-org/sanic/pull/2304) Named tasks and new API for managing background tasks
|
|
||||||
- [#2307](https://github.com/sanic-org/sanic/pull/2307) On app auto-reload, provide insight of changed files
|
|
||||||
- [#2308](https://github.com/sanic-org/sanic/pull/2308) Auto extend application with [Sanic Extensions](https://sanicframework.org/en/plugins/sanic-ext/getting-started.html) if it is installed, and provide first class support for accessing the extensions
|
|
||||||
- [#2309](https://github.com/sanic-org/sanic/pull/2309) Builtin signals changed to `Enum`
|
|
||||||
- [#2313](https://github.com/sanic-org/sanic/pull/2313) Support additional config implementation use case
|
|
||||||
- [#2321](https://github.com/sanic-org/sanic/pull/2321) Refactor environment variable hydration logic
|
|
||||||
- [#2327](https://github.com/sanic-org/sanic/pull/2327) Prevent sending multiple or mixed responses on a single request
|
|
||||||
- [#2330](https://github.com/sanic-org/sanic/pull/2330) Custom type casting on environment variables
|
|
||||||
- [#2332](https://github.com/sanic-org/sanic/pull/2332) Make all deprecation notices consistent
|
|
||||||
- [#2335](https://github.com/sanic-org/sanic/pull/2335) Allow underscore to start instance names
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- [#2273](https://github.com/sanic-org/sanic/pull/2273) Replace assignation by typing for `websocket_handshake`
|
|
||||||
- [#2285](https://github.com/sanic-org/sanic/pull/2285) Fix IPv6 display in startup logs
|
|
||||||
- [#2299](https://github.com/sanic-org/sanic/pull/2299) Dispatch `http.lifecyle.response` from exception handler
|
|
||||||
|
|
||||||
### Deprecations and Removals
|
|
||||||
- [#2306](https://github.com/sanic-org/sanic/pull/2306) Removal of deprecated items
|
|
||||||
- `Sanic` and `Blueprint` may no longer have arbitrary properties attached to them
|
|
||||||
- `Sanic` and `Blueprint` forced to have compliant names
|
|
||||||
- alphanumeric + `_` + `-`
|
|
||||||
- must start with letter or `_`
|
|
||||||
- `load_env` keyword argument of `Sanic`
|
|
||||||
- `sanic.exceptions.abort`
|
|
||||||
- `sanic.views.CompositionView`
|
|
||||||
- `sanic.response.StreamingHTTPResponse`
|
|
||||||
- *NOTE:* the `stream()` response method (where you pass a callable streaming function) has been deprecated and will be removed in v22.6. You should upgrade all streaming responses to the new style: https://sanicframework.org/en/guide/advanced/streaming.html#response-streaming
|
|
||||||
- [#2320](https://github.com/sanic-org/sanic/pull/2320) Remove app instance from Config for error handler setting
|
|
||||||
|
|
||||||
### Developer infrastructure
|
|
||||||
- [#2251](https://github.com/sanic-org/sanic/pull/2251) Change dev install command
|
|
||||||
- [#2286](https://github.com/sanic-org/sanic/pull/2286) Change codeclimate complexity threshold from 5 to 10
|
|
||||||
- [#2287](https://github.com/sanic-org/sanic/pull/2287) Update host test function names so they are not overwritten
|
|
||||||
- [#2292](https://github.com/sanic-org/sanic/pull/2292) Fail CI on error
|
|
||||||
- [#2311](https://github.com/sanic-org/sanic/pull/2311), [#2324](https://github.com/sanic-org/sanic/pull/2324) Do not run tests for draft PRs
|
|
||||||
- [#2336](https://github.com/sanic-org/sanic/pull/2336) Remove paths from coverage checks
|
|
||||||
- [#2338](https://github.com/sanic-org/sanic/pull/2338) Cleanup ports on tests
|
|
||||||
|
|
||||||
### Improved Documentation
|
|
||||||
- [#2269](https://github.com/sanic-org/sanic/pull/2269), [#2329](https://github.com/sanic-org/sanic/pull/2329), [#2333](https://github.com/sanic-org/sanic/pull/2333) Cleanup typos and fix language
|
|
||||||
|
|
||||||
### Miscellaneous
|
|
||||||
- [#2257](https://github.com/sanic-org/sanic/pull/2257), [#2294](https://github.com/sanic-org/sanic/pull/2294), [#2341](https://github.com/sanic-org/sanic/pull/2341) Add Python 3.10 support
|
|
||||||
- [#2279](https://github.com/sanic-org/sanic/pull/2279), [#2317](https://github.com/sanic-org/sanic/pull/2317), [#2322](https://github.com/sanic-org/sanic/pull/2322) Add/correct missing type annotations
|
|
||||||
- [#2305](https://github.com/sanic-org/sanic/pull/2305) Fix examples to use modern implementations
|
|
||||||
@@ -4,14 +4,12 @@ import asyncio
|
|||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
async def notify_server_started_after_five_seconds():
|
async def notify_server_started_after_five_seconds():
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
print("Server successfully started!")
|
print('Server successfully started!')
|
||||||
|
|
||||||
|
|
||||||
app.add_task(notify_server_started_after_five_seconds())
|
app.add_task(notify_server_started_after_five_seconds())
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
from random import randint
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
@app.middleware('request')
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("request")
|
|
||||||
def append_request(request):
|
def append_request(request):
|
||||||
request.ctx.num = randint(0, 100)
|
# Add new key with random value
|
||||||
|
request['num'] = randint(0, 100)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/pop")
|
@app.get('/pop')
|
||||||
def pop_handler(request):
|
def pop_handler(request):
|
||||||
return text(request.ctx.num)
|
# Pop key from request object
|
||||||
|
num = request.pop('num')
|
||||||
|
return text(num)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/key_exist")
|
@app.get('/key_exist')
|
||||||
def key_exist_handler(request):
|
def key_exist_handler(request):
|
||||||
# Check the key is exist or not
|
# Check the key is exist or not
|
||||||
if hasattr(request.ctx, "num"):
|
if 'num' in request:
|
||||||
return text("num exist in request")
|
return text('num exist in request')
|
||||||
|
|
||||||
return text("num does not exist in request")
|
return text('num does not exist in reqeust')
|
||||||
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from functools import wraps
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
def check_request_for_authorization_status(request):
|
def check_request_for_authorization_status(request):
|
||||||
@@ -29,16 +27,14 @@ def authorized(f):
|
|||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
# the user is not authorized.
|
# the user is not authorized.
|
||||||
return json({"status": "not_authorized"}, 403)
|
return json({'status': 'not_authorized'}, 403)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@authorized
|
@authorized
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({"status": "authorized"})
|
return json({'status': 'authorized'})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -1,53 +1,43 @@
|
|||||||
from sanic import Blueprint, Sanic
|
from sanic import Sanic, Blueprint
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
'''
|
||||||
|
Demonstrates that blueprint request middleware are executed in the order they
|
||||||
"""
|
|
||||||
Demonstrates that blueprint request middleware are executed in the order they
|
|
||||||
are added. And blueprint response middleware are executed in _reverse_ order.
|
are added. And blueprint response middleware are executed in _reverse_ order.
|
||||||
On a valid request, it should print "1 2 3 6 5 4" to terminal
|
On a valid request, it should print "1 2 3 6 5 4" to terminal
|
||||||
"""
|
'''
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
bp = Blueprint("bp_example")
|
bp = Blueprint("bp_"+__name__)
|
||||||
|
|
||||||
|
@bp.middleware('request')
|
||||||
@bp.on_request
|
|
||||||
def request_middleware_1(request):
|
def request_middleware_1(request):
|
||||||
print("1")
|
print('1')
|
||||||
|
|
||||||
|
@bp.middleware('request')
|
||||||
@bp.on_request
|
|
||||||
def request_middleware_2(request):
|
def request_middleware_2(request):
|
||||||
print("2")
|
print('2')
|
||||||
|
|
||||||
|
@bp.middleware('request')
|
||||||
@bp.on_request
|
|
||||||
def request_middleware_3(request):
|
def request_middleware_3(request):
|
||||||
print("3")
|
print('3')
|
||||||
|
|
||||||
|
@bp.middleware('response')
|
||||||
@bp.on_response
|
|
||||||
def resp_middleware_4(request, response):
|
def resp_middleware_4(request, response):
|
||||||
print("4")
|
print('4')
|
||||||
|
|
||||||
|
@bp.middleware('response')
|
||||||
@bp.on_response
|
|
||||||
def resp_middleware_5(request, response):
|
def resp_middleware_5(request, response):
|
||||||
print("5")
|
print('5')
|
||||||
|
|
||||||
|
@bp.middleware('response')
|
||||||
@bp.on_response
|
|
||||||
def resp_middleware_6(request, response):
|
def resp_middleware_6(request, response):
|
||||||
print("6")
|
print('6')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
@bp.route("/")
|
|
||||||
def pop_handler(request):
|
def pop_handler(request):
|
||||||
return text("hello world")
|
return text('hello world')
|
||||||
|
|
||||||
|
app.blueprint(bp, url_prefix='/bp')
|
||||||
app.blueprint(bp, url_prefix="/bp")
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)
|
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from sanic import Blueprint, Sanic
|
from sanic import Blueprint, Sanic
|
||||||
from sanic.response import file, json
|
from sanic.response import file, json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
app = Sanic("Example")
|
blueprint = Blueprint("name", url_prefix="/my_blueprint")
|
||||||
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
|
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
|
||||||
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
|
blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3")
|
||||||
blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3")
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/foo")
|
@blueprint.route("/foo")
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ from asyncio import sleep
|
|||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
app = Sanic(__name__, strict_slashes=True)
|
||||||
app = Sanic("DelayedResponseApp", strict_slashes=True)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return response.redirect("/sleep/3")
|
return response.redirect("/sleep/3")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/sleep/<t:number>")
|
@app.get("/sleep/<t:number>")
|
||||||
async def handler2(request, t=0.3):
|
async def handler2(request, t=0.3):
|
||||||
await sleep(t)
|
await sleep(t)
|
||||||
return response.text(f"Slept {t:.1f} seconds.\n")
|
return response.text(f"Slept {t:.1f} seconds.\n")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ and pass in an instance of it when we create our Sanic instance. Inside this
|
|||||||
class' default handler, we can do anything including sending exceptions to
|
class' default handler, we can do anything including sending exceptions to
|
||||||
an external service.
|
an external service.
|
||||||
"""
|
"""
|
||||||
from sanic.exceptions import SanicException
|
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
|
from sanic.exceptions import SanicException
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Imports and code relevant for our CustomHandler class
|
Imports and code relevant for our CustomHandler class
|
||||||
(Ordinarily this would be in a separate file)
|
(Ordinarily this would be in a separate file)
|
||||||
@@ -18,6 +16,7 @@ Imports and code relevant for our CustomHandler class
|
|||||||
|
|
||||||
|
|
||||||
class CustomHandler(ErrorHandler):
|
class CustomHandler(ErrorHandler):
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
# Here, we have access to the exception object
|
# Here, we have access to the exception object
|
||||||
# and can do anything with it (log, send to external service, etc)
|
# and can do anything with it (log, send to external service, etc)
|
||||||
@@ -39,17 +38,17 @@ server's error_handler to an instance of our CustomHandler
|
|||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
handler = CustomHandler()
|
handler = CustomHandler()
|
||||||
app = Sanic("Example", error_handler=handler)
|
app.error_handler = handler
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
# Here, something occurs which causes an unexpected exception
|
# Here, something occurs which causes an unexpected exception
|
||||||
# This exception will flow to our custom handler.
|
# This exception will flow to our custom handler.
|
||||||
raise SanicException("You Broke It!")
|
raise SanicException('You Broke It!')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from sanic import Sanic, response, text
|
from sanic import Sanic, response, text
|
||||||
from sanic.handlers import ErrorHandler
|
|
||||||
from sanic.server.async_server import AsyncioServer
|
|
||||||
|
|
||||||
|
|
||||||
HTTP_PORT = 9999
|
HTTP_PORT = 9999
|
||||||
@@ -34,40 +32,20 @@ def proxy(request, path):
|
|||||||
return response.redirect(url)
|
return response.redirect(url)
|
||||||
|
|
||||||
|
|
||||||
@https.main_process_start
|
@https.listener("main_process_start")
|
||||||
async def start(app, _):
|
async def start(app, _):
|
||||||
http_server = await http.create_server(
|
global http
|
||||||
|
app.http_server = await http.create_server(
|
||||||
port=HTTP_PORT, return_asyncio_server=True
|
port=HTTP_PORT, return_asyncio_server=True
|
||||||
)
|
)
|
||||||
app.add_task(runner(http, http_server))
|
app.http_server.after_start()
|
||||||
app.ctx.http_server = http_server
|
|
||||||
app.ctx.http = http
|
|
||||||
|
|
||||||
|
|
||||||
@https.main_process_stop
|
@https.listener("main_process_stop")
|
||||||
async def stop(app, _):
|
async def stop(app, _):
|
||||||
await app.ctx.http_server.before_stop()
|
app.http_server.before_stop()
|
||||||
await app.ctx.http_server.close()
|
await app.http_server.close()
|
||||||
for connection in app.ctx.http_server.connections:
|
app.http_server.after_stop()
|
||||||
connection.close_if_idle()
|
|
||||||
await app.ctx.http_server.after_stop()
|
|
||||||
app.ctx.http = False
|
|
||||||
|
|
||||||
|
|
||||||
async def runner(app: Sanic, app_server: AsyncioServer):
|
|
||||||
app.is_running = True
|
|
||||||
try:
|
|
||||||
app.signalize()
|
|
||||||
app.finalize()
|
|
||||||
ErrorHandler.finalize(app.error_handler)
|
|
||||||
app_server.init = True
|
|
||||||
|
|
||||||
await app_server.before_start()
|
|
||||||
await app_server.after_start()
|
|
||||||
await app_server.serve_forever()
|
|
||||||
finally:
|
|
||||||
app.is_running = False
|
|
||||||
app.is_stopping = True
|
|
||||||
|
|
||||||
|
|
||||||
https.run(port=HTTPS_PORT, debug=True)
|
https.run(port=HTTPS_PORT, debug=True)
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
sem = None
|
sem = None
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_start
|
@app.listener('before_server_start')
|
||||||
def init(sanic, _):
|
def init(sanic, loop):
|
||||||
global sem
|
global sem
|
||||||
concurrency_per_worker = 4
|
concurrency_per_worker = 4
|
||||||
sem = asyncio.Semaphore(concurrency_per_worker)
|
sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
|
||||||
|
|
||||||
|
|
||||||
async def bounded_fetch(session, url):
|
async def bounded_fetch(session, url):
|
||||||
"""
|
"""
|
||||||
Use session object to perform 'get' request on url
|
Use session object to perform 'get' request on url
|
||||||
"""
|
"""
|
||||||
async with sem:
|
async with sem, session.get(url) as response:
|
||||||
response = await session.get(url)
|
return await response.json()
|
||||||
return response.json()
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@@ -32,9 +28,9 @@ async def test(request):
|
|||||||
"""
|
"""
|
||||||
Download and serve example JSON
|
Download and serve example JSON
|
||||||
"""
|
"""
|
||||||
url = "https://api.github.com/repos/sanic-org/sanic"
|
url = "https://api.github.com/repos/channelcat/sanic"
|
||||||
|
|
||||||
async with httpx.AsyncClient() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await bounded_fetch(session, url)
|
response = await bounded_fetch(session, url)
|
||||||
return json(response)
|
return json(response)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from contextvars import ContextVar
|
import aiotask_context as context
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
@@ -11,8 +11,8 @@ log = logging.getLogger(__name__)
|
|||||||
class RequestIdFilter(logging.Filter):
|
class RequestIdFilter(logging.Filter):
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
try:
|
try:
|
||||||
record.request_id = app.ctx.request_id.get(None) or "n/a"
|
record.request_id = context.get("X-Request-ID")
|
||||||
except AttributeError:
|
except ValueError:
|
||||||
record.request_id = "n/a"
|
record.request_id = "n/a"
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -44,12 +44,13 @@ LOG_SETTINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example", log_config=LOG_SETTINGS)
|
app = Sanic(__name__, log_config=LOG_SETTINGS)
|
||||||
|
|
||||||
|
|
||||||
@app.on_request
|
@app.on_request
|
||||||
async def set_request_id(request):
|
async def set_request_id(request):
|
||||||
request.app.ctx.request_id.set(request.id)
|
request_id = request.id
|
||||||
|
context.set("X-Request-ID", request_id)
|
||||||
log.info(f"Setting {request.id=}")
|
log.info(f"Setting {request.id=}")
|
||||||
|
|
||||||
|
|
||||||
@@ -60,14 +61,14 @@ async def set_request_header(request, response):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
log.debug("X-Request-ID: %s", request.id)
|
log.debug("X-Request-ID: %s", context.get("X-Request-ID"))
|
||||||
log.info("Hello from test!")
|
log.info("Hello from test!")
|
||||||
return response.json({"test": True})
|
return response.json({"test": True})
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_start
|
@app.before_server_start
|
||||||
def setup(app, loop):
|
def setup(app, loop):
|
||||||
app.ctx.request_id = ContextVar("request_id")
|
loop.set_task_factory(context.task_factory)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from platform import node
|
from platform import node
|
||||||
from uuid import getnode as get_mac
|
from uuid import getnode as get_mac
|
||||||
@@ -8,11 +7,10 @@ from uuid import getnode as get_mac
|
|||||||
from logdna import LogDNAHandler
|
from logdna import LogDNAHandler
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.request import Request
|
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
from sanic.request import Request
|
||||||
|
|
||||||
|
log = logging.getLogger('logdna')
|
||||||
log = logging.getLogger("logdna")
|
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
@@ -32,18 +30,16 @@ logdna_options = {
|
|||||||
"index_meta": True,
|
"index_meta": True,
|
||||||
"hostname": node(),
|
"hostname": node(),
|
||||||
"ip": get_my_ip_address(),
|
"ip": get_my_ip_address(),
|
||||||
"mac": get_mac_address(),
|
"mac": get_mac_address()
|
||||||
}
|
}
|
||||||
|
|
||||||
logdna_handler = LogDNAHandler(
|
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
|
||||||
getenv("LOGDNA_API_KEY"), options=logdna_options
|
|
||||||
)
|
|
||||||
|
|
||||||
logdna = logging.getLogger(__name__)
|
logdna = logging.getLogger(__name__)
|
||||||
logdna.setLevel(logging.INFO)
|
logdna.setLevel(logging.INFO)
|
||||||
logdna.addHandler(logdna_handler)
|
logdna.addHandler(logdna_handler)
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware
|
@app.middleware
|
||||||
@@ -53,8 +49,13 @@ def log_request(request: Request):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def default(request):
|
def default(request):
|
||||||
return json({"response": "I was here"})
|
return json({
|
||||||
|
"response": "I was here"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,29 +2,27 @@
|
|||||||
Modify header or status in response
|
Modify header or status in response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
@app.route('/')
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
return response.json(
|
return response.json(
|
||||||
{"message": "Hello world!"},
|
{'message': 'Hello world!'},
|
||||||
headers={"X-Served-By": "sanic"},
|
headers={'X-Served-By': 'sanic'},
|
||||||
status=200,
|
status=200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/unauthorized")
|
@app.route('/unauthorized')
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
return response.json(
|
return response.json(
|
||||||
{"message": "You are not authorized"},
|
{'message': 'You are not authorized'},
|
||||||
headers={"X-Served-By": "sanic"},
|
headers={'X-Served-By': 'sanic'},
|
||||||
status=404,
|
status=404
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def test_port(worker_id):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def app():
|
def app():
|
||||||
app = Sanic("Example")
|
app = Sanic()
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def index(request):
|
async def index(request):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from sanic.handlers import ErrorHandler
|
|||||||
|
|
||||||
|
|
||||||
class RaygunExceptionReporter(ErrorHandler):
|
class RaygunExceptionReporter(ErrorHandler):
|
||||||
|
|
||||||
def __init__(self, raygun_api_key=None):
|
def __init__(self, raygun_api_key=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if raygun_api_key is None:
|
if raygun_api_key is None:
|
||||||
@@ -21,13 +22,16 @@ class RaygunExceptionReporter(ErrorHandler):
|
|||||||
|
|
||||||
|
|
||||||
raygun_error_reporter = RaygunExceptionReporter()
|
raygun_error_reporter = RaygunExceptionReporter()
|
||||||
app = Sanic("Example", error_handler=raygun_error_reporter)
|
app = Sanic(__name__, error_handler=raygun_error_reporter)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/raise")
|
@app.route("/raise")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
raise SanicException("You Broke It!")
|
raise SanicException('You Broke It!')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
app = Sanic("Example")
|
|
||||||
|
@app.route('/')
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def handle_request(request):
|
def handle_request(request):
|
||||||
return response.redirect("/redirect")
|
return response.redirect('/redirect')
|
||||||
|
|
||||||
|
|
||||||
@app.route("/redirect")
|
@app.route('/redirect')
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return response.json({"Redirected": True})
|
return response.json({"Redirected": True})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
@@ -1,63 +1,65 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.views import CompositionView
|
||||||
from sanic.response import stream, text
|
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
from sanic.views import stream as stream_decorator
|
from sanic.views import stream as stream_decorator
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.response import stream, text
|
||||||
|
|
||||||
|
bp = Blueprint('blueprint_request_stream')
|
||||||
bp = Blueprint("bp_example")
|
app = Sanic('request_stream')
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleView(HTTPMethodView):
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
@stream_decorator
|
@stream_decorator
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
result = ""
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
result += body.decode("utf-8")
|
result += body.decode('utf-8')
|
||||||
return text(result)
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/stream", stream=True)
|
@app.post('/stream', stream=True)
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
async def streaming(response):
|
async def streaming(response):
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
body = body.decode("utf-8").replace("1", "A")
|
body = body.decode('utf-8').replace('1', 'A')
|
||||||
await response.write(body)
|
await response.write(body)
|
||||||
|
|
||||||
return stream(streaming)
|
return stream(streaming)
|
||||||
|
|
||||||
|
|
||||||
@bp.put("/bp_stream", stream=True)
|
@bp.put('/bp_stream', stream=True)
|
||||||
async def bp_handler(request):
|
async def bp_handler(request):
|
||||||
result = ""
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
result += body.decode("utf-8").replace("1", "A")
|
result += body.decode('utf-8').replace('1', 'A')
|
||||||
return text(result)
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
async def post_handler(request):
|
async def post_handler(request):
|
||||||
result = ""
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.get()
|
body = await request.stream.get()
|
||||||
if body is None:
|
if body is None:
|
||||||
break
|
break
|
||||||
result += body.decode("utf-8")
|
result += body.decode('utf-8')
|
||||||
return text(result)
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
app.add_route(SimpleView.as_view(), "/method_view")
|
app.add_route(SimpleView.as_view(), '/method_view')
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['POST'], post_handler, stream=True)
|
||||||
|
app.add_route(view, '/composition_view')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from sanic import Sanic
|
||||||
from sanic import Sanic, response
|
from sanic import response
|
||||||
from sanic.config import Config
|
from sanic.config import Config
|
||||||
from sanic.exceptions import RequestTimeout
|
from sanic.exceptions import RequestTimeout
|
||||||
|
|
||||||
|
|
||||||
Config.REQUEST_TIMEOUT = 1
|
Config.REQUEST_TIMEOUT = 1
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route('/')
|
||||||
async def test(request):
|
async def test(request):
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
return response.text("Hello, world!")
|
return response.text('Hello, world!')
|
||||||
|
|
||||||
|
|
||||||
@app.exception(RequestTimeout)
|
@app.exception(RequestTimeout)
|
||||||
def timeout(request, exception):
|
def timeout(request, exception):
|
||||||
return response.text("RequestTimeout from error_handler.", 408)
|
return response.text('RequestTimeout from error_handler.', 408)
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000)
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
from os import getenv
|
|
||||||
|
|
||||||
import rollbar
|
import rollbar
|
||||||
|
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.handlers import ErrorHandler
|
from os import getenv
|
||||||
|
|
||||||
|
|
||||||
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
||||||
|
|
||||||
|
|
||||||
class RollbarExceptionHandler(ErrorHandler):
|
class RollbarExceptionHandler(ErrorHandler):
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
rollbar.report_message(str(exception))
|
rollbar.report_message(str(exception))
|
||||||
return super().default(request, exception)
|
return super().default(request, exception)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example", error_handler=RollbarExceptionHandler())
|
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
|
||||||
|
|
||||||
|
|
||||||
@app.route("/raise")
|
@app.route("/raise")
|
||||||
@@ -25,4 +24,7 @@ def create_error(request):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/text")
|
@app.route("/text")
|
||||||
@@ -59,31 +59,31 @@ async def handler_stream(request):
|
|||||||
return response.stream(body)
|
return response.stream(body)
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_start
|
@app.listener("before_server_start")
|
||||||
async def listener_before_server_start(*args, **kwargs):
|
async def listener_before_server_start(*args, **kwargs):
|
||||||
print("before_server_start")
|
print("before_server_start")
|
||||||
|
|
||||||
|
|
||||||
@app.after_server_start
|
@app.listener("after_server_start")
|
||||||
async def listener_after_server_start(*args, **kwargs):
|
async def listener_after_server_start(*args, **kwargs):
|
||||||
print("after_server_start")
|
print("after_server_start")
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_stop
|
@app.listener("before_server_stop")
|
||||||
async def listener_before_server_stop(*args, **kwargs):
|
async def listener_before_server_stop(*args, **kwargs):
|
||||||
print("before_server_stop")
|
print("before_server_stop")
|
||||||
|
|
||||||
|
|
||||||
@app.after_server_stop
|
@app.listener("after_server_stop")
|
||||||
async def listener_after_server_stop(*args, **kwargs):
|
async def listener_after_server_stop(*args, **kwargs):
|
||||||
print("after_server_stop")
|
print("after_server_stop")
|
||||||
|
|
||||||
|
|
||||||
@app.on_request
|
@app.middleware("request")
|
||||||
async def print_on_request(request):
|
async def print_on_request(request):
|
||||||
print("print_on_request")
|
print("print_on_request")
|
||||||
|
|
||||||
|
|
||||||
@app.on_response
|
@app.middleware("response")
|
||||||
async def print_on_response(request, response):
|
async def print_on_response(request, response):
|
||||||
print("print_on_response")
|
print("print_on_response")
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
from signal import signal, SIGINT
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import uvloop
|
import uvloop
|
||||||
|
|
||||||
from sanic import Sanic, response
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
@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())
|
||||||
async def main():
|
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
server = await app.create_server(
|
loop = asyncio.get_event_loop()
|
||||||
port=8000, host="0.0.0.0", return_asyncio_server=True
|
task = asyncio.ensure_future(server)
|
||||||
)
|
signal(SIGINT, lambda s, f: loop.stop())
|
||||||
|
try:
|
||||||
if server is None:
|
loop.run_forever()
|
||||||
return
|
except:
|
||||||
|
loop.stop()
|
||||||
await server.startup()
|
|
||||||
await server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|||||||
@@ -8,27 +8,12 @@ from sanic import Sanic, response
|
|||||||
from sanic.server import AsyncioServer
|
from sanic.server import AsyncioServer
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_start
|
@app.listener("after_server_start")
|
||||||
async def before_server_start(app, loop):
|
async def after_start_test(app, loop):
|
||||||
print("Async Server starting")
|
print("Async Server Started!")
|
||||||
|
|
||||||
|
|
||||||
@app.after_server_start
|
|
||||||
async def after_server_start(app, loop):
|
|
||||||
print("Async Server started")
|
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_stop
|
|
||||||
async def before_server_stop(app, loop):
|
|
||||||
print("Async Server stopping")
|
|
||||||
|
|
||||||
|
|
||||||
@app.after_server_stop
|
|
||||||
async def after_server_stop(app, loop):
|
|
||||||
print("Async Server stopped")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@@ -43,20 +28,20 @@ serv_coro = app.create_server(
|
|||||||
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)
|
server: AsyncioServer = loop.run_until_complete(serv_task) # type: ignore
|
||||||
loop.run_until_complete(server.startup())
|
server.startup()
|
||||||
|
|
||||||
# When using app.run(), this actually triggers before the serv_coro.
|
# 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
|
# But, in this example, we are using the convenience method, even if it is
|
||||||
# out of order.
|
# out of order.
|
||||||
loop.run_until_complete(server.before_start())
|
server.before_start()
|
||||||
loop.run_until_complete(server.after_start())
|
server.after_start()
|
||||||
try:
|
try:
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
loop.stop()
|
loop.stop()
|
||||||
finally:
|
finally:
|
||||||
loop.run_until_complete(server.before_stop())
|
server.before_stop()
|
||||||
|
|
||||||
# Wait for server to close
|
# Wait for server to close
|
||||||
close_task = server.close()
|
close_task = server.close()
|
||||||
@@ -65,4 +50,4 @@ finally:
|
|||||||
# Complete all tasks on the loop
|
# Complete all tasks on the loop
|
||||||
for connection in server.connections:
|
for connection in server.connections:
|
||||||
connection.close_if_idle()
|
connection.close_if_idle()
|
||||||
loop.run_until_complete(server.after_stop())
|
server.after_stop()
|
||||||
|
|||||||
@@ -6,19 +6,20 @@ from sentry_sdk.integrations.sanic import SanicIntegration
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
sentry_init(
|
sentry_init(
|
||||||
dsn=getenv("SENTRY_DSN"),
|
dsn=getenv("SENTRY_DSN"),
|
||||||
integrations=[SanicIntegration()],
|
integrations=[SanicIntegration()],
|
||||||
)
|
)
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
@app.route("/working")
|
@app.route("/working")
|
||||||
async def working_path(request):
|
async def working_path(request):
|
||||||
return json({"response": "Working API Response"})
|
return json({
|
||||||
|
"response": "Working API Response"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
@@ -27,5 +28,8 @@ async def raise_error(request):
|
|||||||
raise Exception("Testing Sentry Integration")
|
raise Exception("Testing Sentry Integration")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic('some_name')
|
||||||
app = Sanic("some_name")
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleView(HTTPMethodView):
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text("I am get method")
|
return text('I am get method')
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
return text("I am post method")
|
return text('I am post method')
|
||||||
|
|
||||||
def put(self, request):
|
def put(self, request):
|
||||||
return text("I am put method")
|
return text('I am put method')
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
return text("I am patch method")
|
return text('I am patch method')
|
||||||
|
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
return text("I am delete method")
|
return text('I am delete method')
|
||||||
|
|
||||||
|
|
||||||
class SimpleAsyncView(HTTPMethodView):
|
class SimpleAsyncView(HTTPMethodView):
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
return text("I am async get method")
|
return text('I am async get method')
|
||||||
|
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
return text("I am async post method")
|
return text('I am async post method')
|
||||||
|
|
||||||
async def put(self, request):
|
async def put(self, request):
|
||||||
return text("I am async put method")
|
return text('I am async put method')
|
||||||
|
|
||||||
|
|
||||||
app.add_route(SimpleView.as_view(), "/")
|
app.add_route(SimpleView.as_view(), '/')
|
||||||
app.add_route(SimpleAsyncView.as_view(), "/async")
|
app.add_route(SimpleAsyncView.as_view(), '/async')
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@@ -9,5 +9,5 @@ async def test(request):
|
|||||||
return response.json({"test": True})
|
return response.json({"test": True})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
|
|
||||||
app.static("/", "./static")
|
app.static("/", "./static")
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic import response as res
|
from sanic import response as res
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(req):
|
async def test(req):
|
||||||
return res.text("I'm a teapot", status=418)
|
return res.text("I\'m a teapot", status=418)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
from sanic.exceptions import ServerError
|
|
||||||
from sanic.log import logger as log
|
from sanic.log import logger as log
|
||||||
|
from sanic import response
|
||||||
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@@ -13,7 +13,7 @@ async def test_async(request):
|
|||||||
return response.json({"test": True})
|
return response.json({"test": True})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/sync", methods=["GET", "POST"])
|
@app.route("/sync", methods=['GET', 'POST'])
|
||||||
def test_sync(request):
|
def test_sync(request):
|
||||||
return response.json({"test": True})
|
return response.json({"test": True})
|
||||||
|
|
||||||
@@ -31,7 +31,6 @@ def exception(request):
|
|||||||
@app.route("/await")
|
@app.route("/await")
|
||||||
async def test_await(request):
|
async def test_await(request):
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
return response.text("I'm feeling sleepy")
|
return response.text("I'm feeling sleepy")
|
||||||
|
|
||||||
@@ -43,10 +42,8 @@ async def test_file(request):
|
|||||||
|
|
||||||
@app.route("/file_stream")
|
@app.route("/file_stream")
|
||||||
async def test_file_stream(request):
|
async def test_file_stream(request):
|
||||||
return await response.file_stream(
|
return await response.file_stream(os.path.abspath("setup.py"),
|
||||||
os.path.abspath("setup.py"), chunk_size=1024
|
chunk_size=1024)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
# Exceptions
|
# Exceptions
|
||||||
@@ -55,17 +52,14 @@ async def test_file_stream(request):
|
|||||||
|
|
||||||
@app.exception(ServerError)
|
@app.exception(ServerError)
|
||||||
async def test(request, exception):
|
async def test(request, exception):
|
||||||
return response.json(
|
return response.json({"exception": "{}".format(exception), "status": exception.status_code},
|
||||||
{"exception": str(exception), "status": exception.status_code},
|
status=exception.status_code)
|
||||||
status=exception.status_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
# Read from request
|
# Read from request
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
@app.route("/json")
|
@app.route("/json")
|
||||||
def post_json(request):
|
def post_json(request):
|
||||||
return response.json({"received": True, "message": request.json})
|
return response.json({"received": True, "message": request.json})
|
||||||
@@ -73,51 +67,38 @@ def post_json(request):
|
|||||||
|
|
||||||
@app.route("/form")
|
@app.route("/form")
|
||||||
def post_form_json(request):
|
def post_form_json(request):
|
||||||
return response.json(
|
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
|
||||||
{
|
|
||||||
"received": True,
|
|
||||||
"form_data": request.form,
|
|
||||||
"test": request.form.get("test"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/query_string")
|
@app.route("/query_string")
|
||||||
def query_string(request):
|
def query_string(request):
|
||||||
return response.json(
|
return response.json({"parsed": True, "args": request.args, "url": request.url,
|
||||||
{
|
"query_string": request.query_string})
|
||||||
"parsed": True,
|
|
||||||
"args": request.args,
|
|
||||||
"url": request.url,
|
|
||||||
"query_string": request.query_string,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
# Run Server
|
# Run Server
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
@app.before_server_start
|
|
||||||
def before_start(app, loop):
|
def before_start(app, loop):
|
||||||
log.info("SERVER STARTING")
|
log.info("SERVER STARTING")
|
||||||
|
|
||||||
|
|
||||||
@app.after_server_start
|
@app.listener('after_server_start')
|
||||||
def after_start(app, loop):
|
def after_start(app, loop):
|
||||||
log.info("OH OH OH OH OHHHHHHHH")
|
log.info("OH OH OH OH OHHHHHHHH")
|
||||||
|
|
||||||
|
|
||||||
@app.before_server_stop
|
@app.listener('before_server_stop')
|
||||||
def before_stop(app, loop):
|
def before_stop(app, loop):
|
||||||
log.info("SERVER STOPPING")
|
log.info("SERVER STOPPING")
|
||||||
|
|
||||||
|
|
||||||
@app.after_server_stop
|
@app.listener('after_server_stop')
|
||||||
def after_stop(app, loop):
|
def after_stop(app, loop):
|
||||||
log.info("TRIED EVERYTHING")
|
log.info("TRIED EVERYTHING")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import os
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
import socket
|
import socket
|
||||||
|
import os
|
||||||
|
|
||||||
from sanic import Sanic, response
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/test")
|
@app.route("/test")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return response.text("OK")
|
return response.text("OK")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
server_address = './uds_socket'
|
||||||
server_address = "./uds_socket"
|
|
||||||
# Make sure the socket does not already exist
|
# Make sure the socket does not already exist
|
||||||
try:
|
try:
|
||||||
os.unlink(server_address)
|
os.unlink(server_address)
|
||||||
except OSError:
|
except OSError:
|
||||||
if os.path.exists(server_address):
|
if os.path.exists(server_address):
|
||||||
raise
|
raise
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock.bind(server_address)
|
sock.bind(server_address)
|
||||||
app.run(sock=sock)
|
app.run(sock=sock)
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
from sanic import Sanic, response
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
@app.route('/')
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def index(request):
|
async def index(request):
|
||||||
# generate a URL for the endpoint `post_handler`
|
# generate a URL for the endpoint `post_handler`
|
||||||
url = app.url_for("post_handler", post_id=5)
|
url = app.url_for('post_handler', post_id=5)
|
||||||
# the URL is `/posts/5`, redirect to it
|
# the URL is `/posts/5`, redirect to it
|
||||||
return response.redirect(url)
|
return response.redirect(url)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/posts/<post_id>")
|
@app.route('/posts/<post_id>')
|
||||||
async def post_handler(request, post_id):
|
async def post_handler(request, post_id):
|
||||||
return response.text("Post - {}".format(post_id))
|
return response.text('Post - {}'.format(post_id))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ app = Sanic(name="blue-print-group-version-example")
|
|||||||
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
|
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
|
||||||
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
|
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
|
||||||
|
|
||||||
bpg = Blueprint.group(
|
bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
|
||||||
bp1, bp2, url_prefix="/sentient/robot", version=1, strict_slashes=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp1.get("/name")
|
@bp1.get("/name")
|
||||||
@@ -33,5 +31,5 @@ async def bp2_revised_name(request):
|
|||||||
|
|
||||||
app.blueprint(bpg)
|
app.blueprint(bpg)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sanic.blueprints import Blueprint
|
|||||||
# curl -H "Host: bp.example.com" localhost:8000/question
|
# curl -H "Host: bp.example.com" localhost:8000/question
|
||||||
# curl -H "Host: bp.example.com" localhost:8000/answer
|
# curl -H "Host: bp.example.com" localhost:8000/answer
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic(__name__)
|
||||||
bp = Blueprint("bp", host="bp.example.com")
|
bp = Blueprint("bp", host="bp.example.com")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import redirect
|
from sanic.response import redirect
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
app = Sanic("Example")
|
|
||||||
|
|
||||||
|
|
||||||
app.static("index.html", "websocket.html")
|
app.static('index.html', "websocket.html")
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
@app.route("/")
|
|
||||||
def index(request):
|
def index(request):
|
||||||
return redirect("index.html")
|
return redirect("index.html")
|
||||||
|
|
||||||
|
@app.websocket('/feed')
|
||||||
@app.websocket("/feed")
|
|
||||||
async def feed(request, ws):
|
async def feed(request, ws):
|
||||||
while True:
|
while True:
|
||||||
data = "hello!"
|
data = 'hello!'
|
||||||
print("Sending: " + data)
|
print('Sending: ' + data)
|
||||||
await ws.send(data)
|
await ws.send(data)
|
||||||
data = await ws.recv()
|
data = await ws.recv()
|
||||||
print("Received: " + data)
|
print('Received: ' + data)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|
||||||
|
|||||||
6
hack/Dockerfile
Normal file
6
hack/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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,15 +1,196 @@
|
|||||||
from sanic.cli.app import SanicCLI
|
import os
|
||||||
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
import sys
|
||||||
|
|
||||||
|
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||||
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from sanic_routing import __version__ as __routing_version__ # type: ignore
|
||||||
|
|
||||||
|
from sanic import __version__
|
||||||
|
from sanic.app import Sanic
|
||||||
|
from sanic.config import BASE_LOGO
|
||||||
|
from sanic.log import error_logger
|
||||||
|
from sanic.simple import create_simple_server
|
||||||
|
|
||||||
|
|
||||||
if OS_IS_WINDOWS:
|
class SanicArgumentParser(ArgumentParser):
|
||||||
enable_windows_color_support()
|
def add_bool_arguments(self, *args, **kwargs):
|
||||||
|
group = self.add_mutually_exclusive_group()
|
||||||
|
group.add_argument(*args, action="store_true", **kwargs)
|
||||||
|
kwargs["help"] = f"no {kwargs['help']}\n "
|
||||||
|
group.add_argument(
|
||||||
|
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
cli = SanicCLI()
|
parser = SanicArgumentParser(
|
||||||
cli.attach()
|
prog="sanic",
|
||||||
cli.run()
|
description=BASE_LOGO,
|
||||||
|
formatter_class=lambda prog: RawTextHelpFormatter(
|
||||||
|
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(
|
||||||
|
"-H",
|
||||||
|
"--host",
|
||||||
|
dest="host",
|
||||||
|
type=str,
|
||||||
|
default="127.0.0.1",
|
||||||
|
help="Host address [default 127.0.0.1]",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--port",
|
||||||
|
dest="port",
|
||||||
|
type=int,
|
||||||
|
default=8000,
|
||||||
|
help="Port to serve on [default 8000]",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-u",
|
||||||
|
"--unix",
|
||||||
|
dest="unix",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="location of unix socket\n ",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--cert", dest="cert", type=str, help="Location of certificate for SSL"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
|
||||||
|
)
|
||||||
|
parser.add_bool_arguments(
|
||||||
|
"--access-logs", dest="access_log", help="display access logs"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-w",
|
||||||
|
"--workers",
|
||||||
|
dest="workers",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="number of worker processes [default 1]\n ",
|
||||||
|
)
|
||||||
|
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--reload",
|
||||||
|
"--auto-reload",
|
||||||
|
dest="auto_reload",
|
||||||
|
action="store_true",
|
||||||
|
help="Watch source directory for file changes and reload on changes",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-R",
|
||||||
|
"--reload-dir",
|
||||||
|
dest="path",
|
||||||
|
action="append",
|
||||||
|
help="Extra directories to watch and reload on changes\n ",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"module",
|
||||||
|
help=(
|
||||||
|
"Path to your Sanic app. Example: path.to.server:app\n"
|
||||||
|
"If running a Simple Server, path to directory to serve. "
|
||||||
|
"Example: ./\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
module_path = os.path.abspath(os.getcwd())
|
||||||
|
if module_path not in sys.path:
|
||||||
|
sys.path.append(module_path)
|
||||||
|
|
||||||
|
if args.simple:
|
||||||
|
path = Path(args.module)
|
||||||
|
app = create_simple_server(path)
|
||||||
|
else:
|
||||||
|
delimiter = ":" if ":" in args.module else "."
|
||||||
|
module_name, app_name = args.module.rsplit(delimiter, 1)
|
||||||
|
|
||||||
|
if app_name.endswith("()"):
|
||||||
|
args.factory = True
|
||||||
|
app_name = app_name[:-2]
|
||||||
|
|
||||||
|
module = import_module(module_name)
|
||||||
|
app = getattr(module, app_name, None)
|
||||||
|
if args.factory:
|
||||||
|
app = 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:
|
||||||
|
ssl: Optional[Dict[str, Any]] = {
|
||||||
|
"cert": args.cert,
|
||||||
|
"key": args.key,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
ssl = None
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
"host": args.host,
|
||||||
|
"port": args.port,
|
||||||
|
"unix": args.unix,
|
||||||
|
"workers": args.workers,
|
||||||
|
"debug": args.debug,
|
||||||
|
"access_log": args.access_log,
|
||||||
|
"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:
|
||||||
|
if module_name.startswith(e.name):
|
||||||
|
error_logger.error(
|
||||||
|
f"No module named {e.name} found.\n"
|
||||||
|
" Example File: project/sanic_server.py -> app\n"
|
||||||
|
" Example Module: project.sanic_server.app"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
except ValueError:
|
||||||
|
error_logger.exception("Failed to run app")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "21.12.1"
|
__version__ = "21.9.3"
|
||||||
|
|||||||
822
sanic/app.py
822
sanic/app.py
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
from importlib import import_module
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
try:
|
|
||||||
from sanic_ext import Extend # type: ignore
|
|
||||||
except ImportError:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
|
||||||
if not app.config.AUTO_EXTEND:
|
|
||||||
return
|
|
||||||
|
|
||||||
sanic_ext = None
|
|
||||||
with suppress(ModuleNotFoundError):
|
|
||||||
sanic_ext = import_module("sanic_ext")
|
|
||||||
|
|
||||||
if not sanic_ext:
|
|
||||||
if fail:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Sanic Extensions is not installed. You can add it to your "
|
|
||||||
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
|
|
||||||
"install sanic-ext"
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
if not getattr(app, "_ext", None):
|
|
||||||
Ext: Extend = getattr(sanic_ext, "Extend")
|
|
||||||
app._ext = Ext(app, **kwargs)
|
|
||||||
|
|
||||||
return app.ext
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from os import environ
|
|
||||||
|
|
||||||
|
|
||||||
BASE_LOGO = """
|
|
||||||
|
|
||||||
Sanic
|
|
||||||
Build Fast. Run Fast.
|
|
||||||
|
|
||||||
"""
|
|
||||||
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ▄████████▄ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ██ ██▀▀▄ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ███████████ █ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ███████████▄▄▀ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ▀███████▀ \033[0m
|
|
||||||
\033[48;2;255;13;104m \033[0m
|
|
||||||
Dark roast. No sugar."""
|
|
||||||
|
|
||||||
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ▄███ █████ ██ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ▀███████ ███▄ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
|
||||||
\033[38;2;255;255;255;48;2;255;13;104m ████ ████████▀ \033[0m
|
|
||||||
\033[48;2;255;13;104m \033[0m
|
|
||||||
Build Fast. Run Fast."""
|
|
||||||
|
|
||||||
FULL_COLOR_LOGO = """
|
|
||||||
|
|
||||||
\033[38;2;255;13;104m ▄███ █████ ██ \033[0m ▄█▄ ██ █ █ ▄██████████
|
|
||||||
\033[38;2;255;13;104m ██ \033[0m █ █ █ ██ █ █ ██
|
|
||||||
\033[38;2;255;13;104m ▀███████ ███▄ \033[0m ▀ █ █ ██ ▄ █ ██
|
|
||||||
\033[38;2;255;13;104m ██\033[0m █████████ █ ██ █ █ ▄▄
|
|
||||||
\033[38;2;255;13;104m ████ ████████▀ \033[0m █ █ █ ██ █ ▀██ ███████
|
|
||||||
|
|
||||||
""" # noqa
|
|
||||||
|
|
||||||
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
||||||
|
|
||||||
|
|
||||||
def get_logo(full=False, coffee=False):
|
|
||||||
logo = (
|
|
||||||
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
|
|
||||||
if sys.stdout.isatty()
|
|
||||||
else BASE_LOGO
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
sys.platform == "darwin"
|
|
||||||
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
|
|
||||||
):
|
|
||||||
logo = ansi_pattern.sub("", logo)
|
|
||||||
|
|
||||||
return logo
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from shutil import get_terminal_size
|
|
||||||
from textwrap import indent, wrap
|
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
from sanic import __version__
|
|
||||||
from sanic.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
class MOTD(ABC):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
logo: Optional[str],
|
|
||||||
serve_location: str,
|
|
||||||
data: Dict[str, str],
|
|
||||||
extra: Dict[str, str],
|
|
||||||
) -> None:
|
|
||||||
self.logo = logo
|
|
||||||
self.serve_location = serve_location
|
|
||||||
self.data = data
|
|
||||||
self.extra = extra
|
|
||||||
self.key_width = 0
|
|
||||||
self.value_width = 0
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def display(self):
|
|
||||||
... # noqa
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def output(
|
|
||||||
cls,
|
|
||||||
logo: Optional[str],
|
|
||||||
serve_location: str,
|
|
||||||
data: Dict[str, str],
|
|
||||||
extra: Dict[str, str],
|
|
||||||
) -> None:
|
|
||||||
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
|
|
||||||
motd_class(logo, serve_location, data, extra).display()
|
|
||||||
|
|
||||||
|
|
||||||
class MOTDBasic(MOTD):
|
|
||||||
def display(self):
|
|
||||||
if self.logo:
|
|
||||||
logger.debug(self.logo)
|
|
||||||
lines = [f"Sanic v{__version__}"]
|
|
||||||
if self.serve_location:
|
|
||||||
lines.append(f"Goin' Fast @ {self.serve_location}")
|
|
||||||
lines += [
|
|
||||||
*(f"{key}: {value}" for key, value in self.data.items()),
|
|
||||||
*(f"{key}: {value}" for key, value in self.extra.items()),
|
|
||||||
]
|
|
||||||
for line in lines:
|
|
||||||
logger.info(line)
|
|
||||||
|
|
||||||
|
|
||||||
class MOTDTTY(MOTD):
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.set_variables()
|
|
||||||
|
|
||||||
def set_variables(self): # no cov
|
|
||||||
fallback = (108, 24)
|
|
||||||
terminal_width = max(
|
|
||||||
get_terminal_size(fallback=fallback).columns, fallback[0]
|
|
||||||
)
|
|
||||||
self.max_value_width = terminal_width - fallback[0] + 36
|
|
||||||
|
|
||||||
self.key_width = 4
|
|
||||||
self.value_width = self.max_value_width
|
|
||||||
if self.data:
|
|
||||||
self.key_width = max(map(len, self.data.keys()))
|
|
||||||
self.value_width = min(
|
|
||||||
max(map(len, self.data.values())), self.max_value_width
|
|
||||||
)
|
|
||||||
self.logo_lines = self.logo.split("\n") if self.logo else []
|
|
||||||
self.logo_line_length = 24
|
|
||||||
self.centering_length = (
|
|
||||||
self.key_width + self.value_width + 2 + self.logo_line_length
|
|
||||||
)
|
|
||||||
self.display_length = self.key_width + self.value_width + 2
|
|
||||||
|
|
||||||
def display(self):
|
|
||||||
version = f"Sanic v{__version__}".center(self.centering_length)
|
|
||||||
running = (
|
|
||||||
f"Goin' Fast @ {self.serve_location}"
|
|
||||||
if self.serve_location
|
|
||||||
else ""
|
|
||||||
).center(self.centering_length)
|
|
||||||
length = len(version) + 2 - self.logo_line_length
|
|
||||||
first_filler = "─" * (self.logo_line_length - 1)
|
|
||||||
second_filler = "─" * length
|
|
||||||
display_filler = "─" * (self.display_length + 2)
|
|
||||||
lines = [
|
|
||||||
f"\n┌{first_filler}─{second_filler}┐",
|
|
||||||
f"│ {version} │",
|
|
||||||
f"│ {running} │",
|
|
||||||
f"├{first_filler}┬{second_filler}┤",
|
|
||||||
]
|
|
||||||
|
|
||||||
self._render_data(lines, self.data, 0)
|
|
||||||
if self.extra:
|
|
||||||
logo_part = self._get_logo_part(len(lines) - 4)
|
|
||||||
lines.append(f"| {logo_part} ├{display_filler}┤")
|
|
||||||
self._render_data(lines, self.extra, len(lines) - 4)
|
|
||||||
|
|
||||||
self._render_fill(lines)
|
|
||||||
|
|
||||||
lines.append(f"└{first_filler}┴{second_filler}┘\n")
|
|
||||||
logger.info(indent("\n".join(lines), " "))
|
|
||||||
|
|
||||||
def _render_data(self, lines, data, start):
|
|
||||||
offset = 0
|
|
||||||
for idx, (key, value) in enumerate(data.items(), start=start):
|
|
||||||
key = key.rjust(self.key_width)
|
|
||||||
|
|
||||||
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
|
|
||||||
for wrap_index, part in enumerate(wrapped):
|
|
||||||
part = part.ljust(self.value_width)
|
|
||||||
logo_part = self._get_logo_part(idx + offset + wrap_index)
|
|
||||||
display = (
|
|
||||||
f"{key}: {part}"
|
|
||||||
if wrap_index == 0
|
|
||||||
else (" " * len(key) + f" {part}")
|
|
||||||
)
|
|
||||||
lines.append(f"│ {logo_part} │ {display} │")
|
|
||||||
if wrap_index:
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
def _render_fill(self, lines):
|
|
||||||
filler = " " * self.display_length
|
|
||||||
idx = len(lines) - 5
|
|
||||||
for i in range(1, len(self.logo_lines) - idx):
|
|
||||||
logo_part = self.logo_lines[idx + i]
|
|
||||||
lines.append(f"│ {logo_part} │ {filler} │")
|
|
||||||
|
|
||||||
def _get_logo_part(self, idx):
|
|
||||||
try:
|
|
||||||
logo_part = self.logo_lines[idx]
|
|
||||||
except IndexError:
|
|
||||||
logo_part = " " * (self.logo_line_length - 3)
|
|
||||||
return logo_part
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum, auto
|
|
||||||
from pathlib import Path
|
|
||||||
from socket import socket
|
|
||||||
from ssl import SSLContext
|
|
||||||
from typing import TYPE_CHECKING, Any, Optional, Set, Union
|
|
||||||
|
|
||||||
from sanic.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
|
|
||||||
class StrEnum(str, Enum):
|
|
||||||
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
|
||||||
return name.lower()
|
|
||||||
|
|
||||||
|
|
||||||
class Server(StrEnum):
|
|
||||||
SANIC = auto()
|
|
||||||
ASGI = auto()
|
|
||||||
GUNICORN = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class Mode(StrEnum):
|
|
||||||
PRODUCTION = auto()
|
|
||||||
DEBUG = auto()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ApplicationState:
|
|
||||||
app: Sanic
|
|
||||||
asgi: bool = field(default=False)
|
|
||||||
coffee: bool = field(default=False)
|
|
||||||
fast: bool = field(default=False)
|
|
||||||
host: str = field(default="")
|
|
||||||
port: int = field(default=0)
|
|
||||||
ssl: Optional[SSLContext] = field(default=None)
|
|
||||||
sock: Optional[socket] = field(default=None)
|
|
||||||
unix: Optional[str] = field(default=None)
|
|
||||||
mode: Mode = field(default=Mode.PRODUCTION)
|
|
||||||
reload_dirs: Set[Path] = field(default_factory=set)
|
|
||||||
server: Server = field(default=Server.SANIC)
|
|
||||||
is_running: bool = field(default=False)
|
|
||||||
is_started: bool = field(default=False)
|
|
||||||
is_stopping: bool = field(default=False)
|
|
||||||
verbosity: int = field(default=0)
|
|
||||||
workers: int = field(default=0)
|
|
||||||
|
|
||||||
# This property relates to the ApplicationState instance and should
|
|
||||||
# not be changed except in the __post_init__ method
|
|
||||||
_init: bool = field(default=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
self._init = True
|
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Any) -> None:
|
|
||||||
if self._init and name == "_init":
|
|
||||||
raise RuntimeError(
|
|
||||||
"Cannot change the value of _init after instantiation"
|
|
||||||
)
|
|
||||||
super().__setattr__(name, value)
|
|
||||||
if self._init and hasattr(self, f"set_{name}"):
|
|
||||||
getattr(self, f"set_{name}")(value)
|
|
||||||
|
|
||||||
def set_mode(self, value: Union[str, Mode]):
|
|
||||||
if hasattr(self.app, "error_handler"):
|
|
||||||
self.app.error_handler.debug = self.app.debug
|
|
||||||
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_debug(self):
|
|
||||||
return self.mode is Mode.DEBUG
|
|
||||||
@@ -7,11 +7,8 @@ import sanic.app # noqa
|
|||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.helpers import _default
|
|
||||||
from sanic.http import Stage
|
|
||||||
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.response import BaseHTTPResponse
|
|
||||||
from sanic.server import ConnInfo
|
from sanic.server import ConnInfo
|
||||||
from sanic.server.websockets.connection import WebSocketConnection
|
from sanic.server.websockets.connection import WebSocketConnection
|
||||||
|
|
||||||
@@ -54,13 +51,6 @@ class Lifespan:
|
|||||||
await self.asgi_app.sanic_app._server_event("init", "before")
|
await self.asgi_app.sanic_app._server_event("init", "before")
|
||||||
await self.asgi_app.sanic_app._server_event("init", "after")
|
await self.asgi_app.sanic_app._server_event("init", "after")
|
||||||
|
|
||||||
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
|
|
||||||
warnings.warn(
|
|
||||||
"You have set the USE_UVLOOP configuration option, but Sanic "
|
|
||||||
"cannot control the event loop when running in ASGI mode."
|
|
||||||
"This option will be ignored."
|
|
||||||
)
|
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
Gather the listeners to fire on server stop.
|
Gather the listeners to fire on server stop.
|
||||||
@@ -93,8 +83,6 @@ class ASGIApp:
|
|||||||
transport: MockTransport
|
transport: MockTransport
|
||||||
lifespan: Lifespan
|
lifespan: Lifespan
|
||||||
ws: Optional[WebSocketConnection]
|
ws: Optional[WebSocketConnection]
|
||||||
stage: Stage
|
|
||||||
response: Optional[BaseHTTPResponse]
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.ws = None
|
self.ws = None
|
||||||
@@ -107,8 +95,6 @@ class ASGIApp:
|
|||||||
instance.sanic_app = sanic_app
|
instance.sanic_app = sanic_app
|
||||||
instance.transport = MockTransport(scope, receive, send)
|
instance.transport = MockTransport(scope, receive, send)
|
||||||
instance.transport.loop = sanic_app.loop
|
instance.transport.loop = sanic_app.loop
|
||||||
instance.stage = Stage.IDLE
|
|
||||||
instance.response = None
|
|
||||||
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
|
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
|
||||||
|
|
||||||
headers = Header(
|
headers = Header(
|
||||||
@@ -163,8 +149,6 @@ class ASGIApp:
|
|||||||
"""
|
"""
|
||||||
Read and stream the body in chunks from an incoming ASGI message.
|
Read and stream the body in chunks from an incoming ASGI message.
|
||||||
"""
|
"""
|
||||||
if self.stage is Stage.IDLE:
|
|
||||||
self.stage = Stage.REQUEST
|
|
||||||
message = await self.transport.receive()
|
message = await self.transport.receive()
|
||||||
body = message.get("body", b"")
|
body = message.get("body", b"")
|
||||||
if not message.get("more_body", False):
|
if not message.get("more_body", False):
|
||||||
@@ -179,17 +163,11 @@ class ASGIApp:
|
|||||||
if data:
|
if data:
|
||||||
yield data
|
yield data
|
||||||
|
|
||||||
def respond(self, response: BaseHTTPResponse):
|
def respond(self, response):
|
||||||
if self.stage is not Stage.HANDLER:
|
|
||||||
self.stage = Stage.FAILED
|
|
||||||
raise RuntimeError("Response already started")
|
|
||||||
if self.response is not None:
|
|
||||||
self.response.stream = None
|
|
||||||
response.stream, self.response = self, response
|
response.stream, self.response = self, response
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def send(self, data, end_stream):
|
async def send(self, data, end_stream):
|
||||||
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
|
|
||||||
if self.response:
|
if self.response:
|
||||||
response, self.response = self.response, None
|
response, self.response = self.response, None
|
||||||
await self.transport.send(
|
await self.transport.send(
|
||||||
@@ -217,7 +195,6 @@ class ASGIApp:
|
|||||||
Handle the incoming request.
|
Handle the incoming request.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.stage = Stage.HANDLER
|
|
||||||
await self.sanic_app.handle_request(self.request)
|
await self.sanic_app.handle_request(self.request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.sanic_app.handle_exception(self.request, e)
|
await self.sanic_app.handle_exception(self.request, e)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Tuple
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.exceptions import SanicException
|
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
|
||||||
@@ -11,7 +11,7 @@ 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_\-]*$")
|
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$")
|
||||||
|
|
||||||
|
|
||||||
class BaseSanic(
|
class BaseSanic(
|
||||||
@@ -20,11 +20,10 @@ class BaseSanic(
|
|||||||
ListenerMixin,
|
ListenerMixin,
|
||||||
ExceptionMixin,
|
ExceptionMixin,
|
||||||
SignalMixin,
|
SignalMixin,
|
||||||
metaclass=SanicMeta,
|
|
||||||
):
|
):
|
||||||
__slots__ = ("name",)
|
__fake_slots__: Tuple[str, ...]
|
||||||
|
|
||||||
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, name: str = None, *args, **kwargs) -> None:
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
@@ -34,10 +33,11 @@ class BaseSanic(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not VALID_NAME.match(name):
|
if not VALID_NAME.match(name):
|
||||||
raise SanicException(
|
warn(
|
||||||
f"{class_name} instance named '{name}' uses an invalid "
|
f"{class_name} instance named '{name}' uses a format that is"
|
||||||
"format. Names must begin with a character and may only "
|
f"deprecated. Starting in version 21.12, {class_name} objects "
|
||||||
"contain alphanumeric characters, _, or -."
|
"must be named only using alphanumeric characters, _, or -.",
|
||||||
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -52,12 +52,15 @@ class BaseSanic(
|
|||||||
return f'{self.__class__.__name__}(name="{self.name}")'
|
return f'{self.__class__.__name__}(name="{self.name}")'
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Any) -> None:
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
try:
|
# This is a temporary compat layer so we can raise a warning until
|
||||||
super().__setattr__(name, value)
|
# setting attributes on the app instance can be removed and deprecated
|
||||||
except AttributeError as e:
|
# with a proper implementation of __slots__
|
||||||
raise AttributeError(
|
if name not in self.__fake_slots__:
|
||||||
|
warn(
|
||||||
f"Setting variables on {self.__class__.__name__} instances is "
|
f"Setting variables on {self.__class__.__name__} instances is "
|
||||||
"not allowed. You should change your "
|
"deprecated and will be removed in version 21.12. You should "
|
||||||
f"{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.",
|
||||||
) from e
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
super().__setattr__(name, value)
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
class SanicMeta(type):
|
|
||||||
@classmethod
|
|
||||||
def __prepare__(metaclass, name, bases, **kwds):
|
|
||||||
cls = super().__prepare__(metaclass, name, bases, **kwds)
|
|
||||||
cls["__slots__"] = ()
|
|
||||||
return cls
|
|
||||||
@@ -5,7 +5,7 @@ from functools import partial
|
|||||||
from typing import TYPE_CHECKING, List, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ class BlueprintGroup(MutableSequence):
|
|||||||
:param args: List of Python exceptions to be caught by the handler
|
:param args: List of Python exceptions to be caught by the handler
|
||||||
:param kwargs: Additional optional arguments to be passed to the
|
:param kwargs: Additional optional arguments to be passed to the
|
||||||
exception handler
|
exception handler
|
||||||
:return: a decorated method to handle global exceptions for any
|
:return a decorated method to handle global exceptions for any
|
||||||
blueprint registered under this group.
|
blueprint registered under this group.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,13 @@ import asyncio
|
|||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from functools import wraps
|
|
||||||
from inspect import isfunction
|
|
||||||
from itertools import chain
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import (
|
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
from sanic_routing.exceptions import NotFound # type: ignore
|
from sanic_routing.exceptions import NotFound # type: ignore
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic.base.root 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.helpers import Default, _default
|
||||||
@@ -36,34 +22,8 @@ from sanic.models.handler_types import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic # noqa
|
||||||
|
|
||||||
|
|
||||||
def lazy(func, as_decorator=True):
|
|
||||||
@wraps(func)
|
|
||||||
def decorator(bp, *args, **kwargs):
|
|
||||||
nonlocal as_decorator
|
|
||||||
kwargs["apply"] = False
|
|
||||||
pass_handler = None
|
|
||||||
|
|
||||||
if args and isfunction(args[0]):
|
|
||||||
as_decorator = False
|
|
||||||
|
|
||||||
def wrapper(handler):
|
|
||||||
future = func(bp, *args, **kwargs)
|
|
||||||
if as_decorator:
|
|
||||||
future = future(handler)
|
|
||||||
|
|
||||||
if bp.registered:
|
|
||||||
for app in bp.apps:
|
|
||||||
bp.register(app, {})
|
|
||||||
|
|
||||||
return future
|
|
||||||
|
|
||||||
return wrapper if as_decorator else wrapper(pass_handler)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
class Blueprint(BaseSanic):
|
class Blueprint(BaseSanic):
|
||||||
@@ -79,13 +39,13 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
:param name: unique name of the blueprint
|
:param name: unique name of the blueprint
|
||||||
:param url_prefix: URL to be prefixed before all route URLs
|
:param url_prefix: URL to be prefixed before all route URLs
|
||||||
:param host: IP Address or 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 */*
|
trailing */*
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__fake_slots__ = (
|
||||||
"_apps",
|
"_apps",
|
||||||
"_future_routes",
|
"_future_routes",
|
||||||
"_future_statics",
|
"_future_statics",
|
||||||
@@ -98,6 +58,7 @@ class Blueprint(BaseSanic):
|
|||||||
"host",
|
"host",
|
||||||
"listeners",
|
"listeners",
|
||||||
"middlewares",
|
"middlewares",
|
||||||
|
"name",
|
||||||
"routes",
|
"routes",
|
||||||
"statics",
|
"statics",
|
||||||
"strict_slashes",
|
"strict_slashes",
|
||||||
@@ -111,7 +72,7 @@ class Blueprint(BaseSanic):
|
|||||||
self,
|
self,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
url_prefix: Optional[str] = None,
|
url_prefix: Optional[str] = None,
|
||||||
host: Optional[Union[List[str], 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",
|
version_prefix: str = "/v",
|
||||||
@@ -154,21 +115,34 @@ class Blueprint(BaseSanic):
|
|||||||
)
|
)
|
||||||
return self._apps
|
return self._apps
|
||||||
|
|
||||||
@property
|
def route(self, *args, **kwargs):
|
||||||
def registered(self) -> bool:
|
kwargs["apply"] = False
|
||||||
return bool(self._apps)
|
return super().route(*args, **kwargs)
|
||||||
|
|
||||||
exception = lazy(BaseSanic.exception)
|
def static(self, *args, **kwargs):
|
||||||
listener = lazy(BaseSanic.listener)
|
kwargs["apply"] = False
|
||||||
middleware = lazy(BaseSanic.middleware)
|
return super().static(*args, **kwargs)
|
||||||
route = lazy(BaseSanic.route)
|
|
||||||
signal = lazy(BaseSanic.signal)
|
def middleware(self, *args, **kwargs):
|
||||||
static = lazy(BaseSanic.static, as_decorator=False)
|
kwargs["apply"] = False
|
||||||
|
return super().middleware(*args, **kwargs)
|
||||||
|
|
||||||
|
def listener(self, *args, **kwargs):
|
||||||
|
kwargs["apply"] = False
|
||||||
|
return super().listener(*args, **kwargs)
|
||||||
|
|
||||||
|
def exception(self, *args, **kwargs):
|
||||||
|
kwargs["apply"] = False
|
||||||
|
return super().exception(*args, **kwargs)
|
||||||
|
|
||||||
|
def signal(self, event: str, *args, **kwargs):
|
||||||
|
kwargs["apply"] = False
|
||||||
|
return super().signal(event, *args, **kwargs)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._apps: Set[Sanic] = set()
|
self._apps: Set[Sanic] = set()
|
||||||
self.exceptions: List[RouteHandler] = []
|
self.exceptions: List[RouteHandler] = []
|
||||||
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
|
self.listeners: Dict[str, List[ListenerType]] = {}
|
||||||
self.middlewares: List[MiddlewareType] = []
|
self.middlewares: List[MiddlewareType] = []
|
||||||
self.routes: List[Route] = []
|
self.routes: List[Route] = []
|
||||||
self.statics: List[RouteHandler] = []
|
self.statics: List[RouteHandler] = []
|
||||||
@@ -247,7 +221,7 @@ class Blueprint(BaseSanic):
|
|||||||
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",
|
version_prefix: str = "/v",
|
||||||
) -> BlueprintGroup:
|
):
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
@@ -300,7 +274,6 @@ class Blueprint(BaseSanic):
|
|||||||
middleware = []
|
middleware = []
|
||||||
exception_handlers = []
|
exception_handlers = []
|
||||||
listeners = defaultdict(list)
|
listeners = defaultdict(list)
|
||||||
registered = set()
|
|
||||||
|
|
||||||
# Routes
|
# Routes
|
||||||
for future in self._future_routes:
|
for future in self._future_routes:
|
||||||
@@ -327,15 +300,12 @@ class Blueprint(BaseSanic):
|
|||||||
)
|
)
|
||||||
|
|
||||||
name = app._generate_name(future.name)
|
name = app._generate_name(future.name)
|
||||||
host = future.host or self.host
|
|
||||||
if isinstance(host, list):
|
|
||||||
host = tuple(host)
|
|
||||||
|
|
||||||
apply_route = FutureRoute(
|
apply_route = FutureRoute(
|
||||||
future.handler,
|
future.handler,
|
||||||
uri[1:] if uri.startswith("//") else uri,
|
uri[1:] if uri.startswith("//") else uri,
|
||||||
future.methods,
|
future.methods,
|
||||||
host,
|
future.host or self.host,
|
||||||
strict_slashes,
|
strict_slashes,
|
||||||
future.stream,
|
future.stream,
|
||||||
version,
|
version,
|
||||||
@@ -347,13 +317,8 @@ class Blueprint(BaseSanic):
|
|||||||
future.static,
|
future.static,
|
||||||
version_prefix,
|
version_prefix,
|
||||||
error_format,
|
error_format,
|
||||||
future.route_context,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (self, apply_route) in app._future_registry:
|
|
||||||
continue
|
|
||||||
|
|
||||||
registered.add(apply_route)
|
|
||||||
route = app._apply_route(apply_route)
|
route = app._apply_route(apply_route)
|
||||||
operation = (
|
operation = (
|
||||||
routes.extend if isinstance(route, list) else routes.append
|
routes.extend if isinstance(route, list) else routes.append
|
||||||
@@ -365,11 +330,6 @@ 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
|
||||||
apply_route = FutureStatic(uri, *future[1:])
|
apply_route = FutureStatic(uri, *future[1:])
|
||||||
|
|
||||||
if (self, apply_route) in app._future_registry:
|
|
||||||
continue
|
|
||||||
|
|
||||||
registered.add(apply_route)
|
|
||||||
route = app._apply_static(apply_route)
|
route = app._apply_static(apply_route)
|
||||||
routes.append(route)
|
routes.append(route)
|
||||||
|
|
||||||
@@ -378,56 +338,34 @@ class Blueprint(BaseSanic):
|
|||||||
if route_names:
|
if route_names:
|
||||||
# Middleware
|
# Middleware
|
||||||
for future in self._future_middleware:
|
for future in self._future_middleware:
|
||||||
if (self, future) in app._future_registry:
|
|
||||||
continue
|
|
||||||
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:
|
||||||
if (self, future) in app._future_registry:
|
|
||||||
continue
|
|
||||||
exception_handlers.append(
|
exception_handlers.append(
|
||||||
app._apply_exception_handler(future, route_names)
|
app._apply_exception_handler(future, route_names)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Event listeners
|
# Event listeners
|
||||||
for future in self._future_listeners:
|
for listener in self._future_listeners:
|
||||||
if (self, future) in app._future_registry:
|
listeners[listener.event].append(app._apply_listener(listener))
|
||||||
continue
|
|
||||||
listeners[future.event].append(app._apply_listener(future))
|
|
||||||
|
|
||||||
# Signals
|
# Signals
|
||||||
for future in self._future_signals:
|
for signal in self._future_signals:
|
||||||
if (self, future) in app._future_registry:
|
signal.condition.update({"blueprint": self.name})
|
||||||
continue
|
app._apply_signal(signal)
|
||||||
future.condition.update({"__blueprint__": self.name})
|
|
||||||
# Force exclusive to be False
|
|
||||||
app._apply_signal(tuple((*future[:-1], False)))
|
|
||||||
|
|
||||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
self.routes = [route for route in routes if isinstance(route, Route)]
|
||||||
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
|
||||||
]
|
]
|
||||||
self.middlewares += middleware
|
self.middlewares = middleware
|
||||||
self.exceptions += exception_handlers
|
self.exceptions = exception_handlers
|
||||||
self.listeners.update(dict(listeners))
|
self.listeners = dict(listeners)
|
||||||
|
|
||||||
if self.registered:
|
|
||||||
self.register_futures(
|
|
||||||
self.apps,
|
|
||||||
self,
|
|
||||||
chain(
|
|
||||||
registered,
|
|
||||||
self._future_middleware,
|
|
||||||
self._future_exceptions,
|
|
||||||
self._future_listeners,
|
|
||||||
self._future_signals,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def dispatch(self, *args, **kwargs):
|
async def dispatch(self, *args, **kwargs):
|
||||||
condition = kwargs.pop("condition", {})
|
condition = kwargs.pop("condition", {})
|
||||||
condition.update({"__blueprint__": self.name})
|
condition.update({"blueprint": self.name})
|
||||||
kwargs["condition"] = condition
|
kwargs["condition"] = condition
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
||||||
@@ -455,10 +393,3 @@ class Blueprint(BaseSanic):
|
|||||||
value = v
|
value = v
|
||||||
break
|
break
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def register_futures(
|
|
||||||
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
|
|
||||||
):
|
|
||||||
for app in apps:
|
|
||||||
app._future_registry.update(set((bp, item) for item in futures))
|
|
||||||
|
|||||||
189
sanic/cli/app.py
189
sanic/cli/app.py
@@ -1,189 +0,0 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
||||||
from importlib import import_module
|
|
||||||
from pathlib import Path
|
|
||||||
from textwrap import indent
|
|
||||||
from typing import Any, List, Union
|
|
||||||
|
|
||||||
from sanic.app import Sanic
|
|
||||||
from sanic.application.logo import get_logo
|
|
||||||
from sanic.cli.arguments import Group
|
|
||||||
from sanic.log import error_logger
|
|
||||||
from sanic.simple import create_simple_server
|
|
||||||
|
|
||||||
|
|
||||||
class SanicArgumentParser(ArgumentParser):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class SanicCLI:
|
|
||||||
DESCRIPTION = indent(
|
|
||||||
f"""
|
|
||||||
{get_logo(True)}
|
|
||||||
|
|
||||||
To start running a Sanic application, provide a path to the module, where
|
|
||||||
app is a Sanic() instance:
|
|
||||||
|
|
||||||
$ sanic path.to.server:app
|
|
||||||
|
|
||||||
Or, a path to a callable that returns a Sanic() instance:
|
|
||||||
|
|
||||||
$ sanic path.to.factory:create_app --factory
|
|
||||||
|
|
||||||
Or, a path to a directory to run as a simple HTTP server:
|
|
||||||
|
|
||||||
$ sanic ./path/to/static --simple
|
|
||||||
""",
|
|
||||||
prefix=" ",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
width = shutil.get_terminal_size().columns
|
|
||||||
self.parser = SanicArgumentParser(
|
|
||||||
prog="sanic",
|
|
||||||
description=self.DESCRIPTION,
|
|
||||||
formatter_class=lambda prog: RawTextHelpFormatter(
|
|
||||||
prog,
|
|
||||||
max_help_position=36 if width > 96 else 24,
|
|
||||||
indent_increment=4,
|
|
||||||
width=None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.parser._positionals.title = "Required\n========\n Positional"
|
|
||||||
self.parser._optionals.title = "Optional\n========\n General"
|
|
||||||
self.main_process = (
|
|
||||||
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
|
|
||||||
)
|
|
||||||
self.args: List[Any] = []
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
for group in Group._registry:
|
|
||||||
group.create(self.parser).attach()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# This is to provide backwards compat -v to display version
|
|
||||||
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
|
|
||||||
parse_args = ["--version"] if legacy_version else None
|
|
||||||
|
|
||||||
self.args = self.parser.parse_args(args=parse_args)
|
|
||||||
self._precheck()
|
|
||||||
|
|
||||||
try:
|
|
||||||
app = self._get_app()
|
|
||||||
kwargs = self._build_run_kwargs()
|
|
||||||
app.run(**kwargs)
|
|
||||||
except ValueError:
|
|
||||||
error_logger.exception("Failed to run app")
|
|
||||||
|
|
||||||
def _precheck(self):
|
|
||||||
if self.args.debug and self.main_process:
|
|
||||||
error_logger.warning(
|
|
||||||
"Starting in v22.3, --debug will no "
|
|
||||||
"longer automatically run the auto-reloader.\n Switch to "
|
|
||||||
"--dev to continue using that functionality."
|
|
||||||
)
|
|
||||||
|
|
||||||
# # Custom TLS mismatch handling for better diagnostics
|
|
||||||
if self.main_process and (
|
|
||||||
# one of cert/key missing
|
|
||||||
bool(self.args.cert) != bool(self.args.key)
|
|
||||||
# new and old style self.args used together
|
|
||||||
or self.args.tls
|
|
||||||
and self.args.cert
|
|
||||||
# strict host checking without certs would always fail
|
|
||||||
or self.args.tlshost
|
|
||||||
and not self.args.tls
|
|
||||||
and not self.args.cert
|
|
||||||
):
|
|
||||||
self.parser.print_usage(sys.stderr)
|
|
||||||
message = (
|
|
||||||
"TLS certificates must be specified by either of:\n"
|
|
||||||
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
|
|
||||||
" --tls certdir (equivalent to the above)"
|
|
||||||
)
|
|
||||||
error_logger.error(message)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def _get_app(self):
|
|
||||||
try:
|
|
||||||
module_path = os.path.abspath(os.getcwd())
|
|
||||||
if module_path not in sys.path:
|
|
||||||
sys.path.append(module_path)
|
|
||||||
|
|
||||||
if self.args.simple:
|
|
||||||
path = Path(self.args.module)
|
|
||||||
app = create_simple_server(path)
|
|
||||||
else:
|
|
||||||
delimiter = ":" if ":" in self.args.module else "."
|
|
||||||
module_name, app_name = self.args.module.rsplit(delimiter, 1)
|
|
||||||
|
|
||||||
if app_name.endswith("()"):
|
|
||||||
self.args.factory = True
|
|
||||||
app_name = app_name[:-2]
|
|
||||||
|
|
||||||
module = import_module(module_name)
|
|
||||||
app = getattr(module, app_name, None)
|
|
||||||
if self.args.factory:
|
|
||||||
app = app()
|
|
||||||
|
|
||||||
app_type_name = type(app).__name__
|
|
||||||
|
|
||||||
if not isinstance(app, Sanic):
|
|
||||||
raise ValueError(
|
|
||||||
f"Module is not a Sanic app, it is a {app_type_name}\n"
|
|
||||||
f" Perhaps you meant {self.args.module}.app?"
|
|
||||||
)
|
|
||||||
except ImportError as e:
|
|
||||||
if module_name.startswith(e.name):
|
|
||||||
error_logger.error(
|
|
||||||
f"No module named {e.name} found.\n"
|
|
||||||
" Example File: project/sanic_server.py -> app\n"
|
|
||||||
" Example Module: project.sanic_server.app"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
return app
|
|
||||||
|
|
||||||
def _build_run_kwargs(self):
|
|
||||||
ssl: Union[None, dict, str, list] = []
|
|
||||||
if self.args.tlshost:
|
|
||||||
ssl.append(None)
|
|
||||||
if self.args.cert is not None or self.args.key is not None:
|
|
||||||
ssl.append(dict(cert=self.args.cert, key=self.args.key))
|
|
||||||
if self.args.tls:
|
|
||||||
ssl += self.args.tls
|
|
||||||
if not ssl:
|
|
||||||
ssl = None
|
|
||||||
elif len(ssl) == 1 and ssl[0] is not None:
|
|
||||||
# Use only one cert, no TLSSelector.
|
|
||||||
ssl = ssl[0]
|
|
||||||
kwargs = {
|
|
||||||
"access_log": self.args.access_log,
|
|
||||||
"debug": self.args.debug,
|
|
||||||
"fast": self.args.fast,
|
|
||||||
"host": self.args.host,
|
|
||||||
"motd": self.args.motd,
|
|
||||||
"noisy_exceptions": self.args.noisy_exceptions,
|
|
||||||
"port": self.args.port,
|
|
||||||
"ssl": ssl,
|
|
||||||
"unix": self.args.unix,
|
|
||||||
"verbosity": self.args.verbosity or 0,
|
|
||||||
"workers": self.args.workers,
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.args.auto_reload:
|
|
||||||
kwargs["auto_reload"] = True
|
|
||||||
|
|
||||||
if self.args.path:
|
|
||||||
if self.args.auto_reload or self.args.debug:
|
|
||||||
kwargs["reload_dir"] = self.args.path
|
|
||||||
else:
|
|
||||||
error_logger.warning(
|
|
||||||
"Ignoring '--reload-dir' since auto reloading was not "
|
|
||||||
"enabled. If you would like to watch directories for "
|
|
||||||
"changes, consider using --debug or --auto-reload."
|
|
||||||
)
|
|
||||||
return kwargs
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from argparse import ArgumentParser, _ArgumentGroup
|
|
||||||
from typing import List, Optional, Type, Union
|
|
||||||
|
|
||||||
from sanic_routing import __version__ as __routing_version__ # type: ignore
|
|
||||||
|
|
||||||
from sanic import __version__
|
|
||||||
|
|
||||||
|
|
||||||
class Group:
|
|
||||||
name: Optional[str]
|
|
||||||
container: Union[ArgumentParser, _ArgumentGroup]
|
|
||||||
_registry: List[Type[Group]] = []
|
|
||||||
|
|
||||||
def __init_subclass__(cls) -> None:
|
|
||||||
Group._registry.append(cls)
|
|
||||||
|
|
||||||
def __init__(self, parser: ArgumentParser, title: Optional[str]):
|
|
||||||
self.parser = parser
|
|
||||||
|
|
||||||
if title:
|
|
||||||
self.container = self.parser.add_argument_group(title=f" {title}")
|
|
||||||
else:
|
|
||||||
self.container = self.parser
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create(cls, parser: ArgumentParser):
|
|
||||||
instance = cls(parser, cls.name)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def add_bool_arguments(self, *args, **kwargs):
|
|
||||||
group = self.container.add_mutually_exclusive_group()
|
|
||||||
kwargs["help"] = kwargs["help"].capitalize()
|
|
||||||
group.add_argument(*args, action="store_true", **kwargs)
|
|
||||||
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
|
|
||||||
group.add_argument(
|
|
||||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GeneralGroup(Group):
|
|
||||||
name = None
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
self.container.add_argument(
|
|
||||||
"--version",
|
|
||||||
action="version",
|
|
||||||
version=f"Sanic {__version__}; Routing {__routing_version__}",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.container.add_argument(
|
|
||||||
"module",
|
|
||||||
help=(
|
|
||||||
"Path to your Sanic app. Example: path.to.server:app\n"
|
|
||||||
"If running a Simple Server, path to directory to serve. "
|
|
||||||
"Example: ./\n"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationGroup(Group):
|
|
||||||
name = "Application"
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
self.container.add_argument(
|
|
||||||
"--factory",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Treat app as an application factory, "
|
|
||||||
"i.e. a () -> <Sanic app> callable"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"-s",
|
|
||||||
"--simple",
|
|
||||||
dest="simple",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Run Sanic as a Simple Server, and serve the contents of "
|
|
||||||
"a directory\n(module arg should be a path)"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SocketGroup(Group):
|
|
||||||
name = "Socket binding"
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
self.container.add_argument(
|
|
||||||
"-H",
|
|
||||||
"--host",
|
|
||||||
dest="host",
|
|
||||||
type=str,
|
|
||||||
default="127.0.0.1",
|
|
||||||
help="Host address [default 127.0.0.1]",
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"-p",
|
|
||||||
"--port",
|
|
||||||
dest="port",
|
|
||||||
type=int,
|
|
||||||
default=8000,
|
|
||||||
help="Port to serve on [default 8000]",
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"-u",
|
|
||||||
"--unix",
|
|
||||||
dest="unix",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help="location of unix socket",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TLSGroup(Group):
|
|
||||||
name = "TLS certificate"
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
self.container.add_argument(
|
|
||||||
"--cert",
|
|
||||||
dest="cert",
|
|
||||||
type=str,
|
|
||||||
help="Location of fullchain.pem, bundle.crt or equivalent",
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"--key",
|
|
||||||
dest="key",
|
|
||||||
type=str,
|
|
||||||
help="Location of privkey.pem or equivalent .key file",
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"--tls",
|
|
||||||
metavar="DIR",
|
|
||||||
type=str,
|
|
||||||
action="append",
|
|
||||||
help=(
|
|
||||||
"TLS certificate folder with fullchain.pem and privkey.pem\n"
|
|
||||||
"May be specified multiple times to choose multiple "
|
|
||||||
"certificates"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"--tls-strict-host",
|
|
||||||
dest="tlshost",
|
|
||||||
action="store_true",
|
|
||||||
help="Only allow clients that send an SNI matching server certs",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkerGroup(Group):
|
|
||||||
name = "Worker"
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
group = self.container.add_mutually_exclusive_group()
|
|
||||||
group.add_argument(
|
|
||||||
"-w",
|
|
||||||
"--workers",
|
|
||||||
dest="workers",
|
|
||||||
type=int,
|
|
||||||
default=1,
|
|
||||||
help="Number of worker processes [default 1]",
|
|
||||||
)
|
|
||||||
group.add_argument(
|
|
||||||
"--fast",
|
|
||||||
dest="fast",
|
|
||||||
action="store_true",
|
|
||||||
help="Set the number of workers to max allowed",
|
|
||||||
)
|
|
||||||
self.add_bool_arguments(
|
|
||||||
"--access-logs", dest="access_log", help="display access logs"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentGroup(Group):
|
|
||||||
name = "Development"
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
self.container.add_argument(
|
|
||||||
"--debug",
|
|
||||||
dest="debug",
|
|
||||||
action="store_true",
|
|
||||||
help="Run the server in debug mode",
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"-d",
|
|
||||||
"--dev",
|
|
||||||
dest="debug",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Currently is an alias for --debug. But starting in v22.3, \n"
|
|
||||||
"--debug will no longer automatically trigger auto_restart. \n"
|
|
||||||
"However, --dev will continue, effectively making it the \n"
|
|
||||||
"same as debug + auto_reload."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"-r",
|
|
||||||
"--reload",
|
|
||||||
"--auto-reload",
|
|
||||||
dest="auto_reload",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Watch source directory for file changes and reload on "
|
|
||||||
"changes"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"-R",
|
|
||||||
"--reload-dir",
|
|
||||||
dest="path",
|
|
||||||
action="append",
|
|
||||||
help="Extra directories to watch and reload on changes",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OutputGroup(Group):
|
|
||||||
name = "Output"
|
|
||||||
|
|
||||||
def attach(self):
|
|
||||||
self.add_bool_arguments(
|
|
||||||
"--motd",
|
|
||||||
dest="motd",
|
|
||||||
default=True,
|
|
||||||
help="Show the startup display",
|
|
||||||
)
|
|
||||||
self.container.add_argument(
|
|
||||||
"-v",
|
|
||||||
"--verbosity",
|
|
||||||
action="count",
|
|
||||||
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
|
|
||||||
)
|
|
||||||
self.add_bool_arguments(
|
|
||||||
"--noisy-exceptions",
|
|
||||||
dest="noisy_exceptions",
|
|
||||||
help="Output stack traces for all exceptions",
|
|
||||||
)
|
|
||||||
@@ -8,21 +8,6 @@ from multidict import CIMultiDict # type: ignore
|
|||||||
|
|
||||||
|
|
||||||
OS_IS_WINDOWS = os.name == "nt"
|
OS_IS_WINDOWS = os.name == "nt"
|
||||||
UVLOOP_INSTALLED = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import uvloop # type: ignore # noqa
|
|
||||||
|
|
||||||
UVLOOP_INSTALLED = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def enable_windows_color_support():
|
|
||||||
import ctypes
|
|
||||||
|
|
||||||
kernel = ctypes.windll.kernel32
|
|
||||||
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
|
|
||||||
|
|
||||||
|
|
||||||
class Header(CIMultiDict):
|
class Header(CIMultiDict):
|
||||||
|
|||||||
191
sanic/config.py
191
sanic/config.py
@@ -1,34 +1,38 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from inspect import getmembers, isclass, isdatadescriptor
|
from inspect import isclass
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
from sanic.errorpages import check_error_format
|
||||||
from sanic.helpers import Default, _default
|
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
from sanic.log import deprecation, error_logger
|
|
||||||
from sanic.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 = """
|
||||||
|
|
||||||
|
Sanic
|
||||||
|
Build Fast. Run Fast.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"_FALLBACK_ERROR_FORMAT": _default,
|
|
||||||
"ACCESS_LOG": True,
|
"ACCESS_LOG": True,
|
||||||
"AUTO_EXTEND": True,
|
|
||||||
"AUTO_RELOAD": False,
|
|
||||||
"EVENT_AUTOREGISTER": False,
|
"EVENT_AUTOREGISTER": False,
|
||||||
|
"FALLBACK_ERROR_FORMAT": "auto",
|
||||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
"FORWARDED_SECRET": None,
|
"FORWARDED_SECRET": None,
|
||||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||||
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||||
"KEEP_ALIVE": True,
|
"KEEP_ALIVE": True,
|
||||||
"MOTD": True,
|
|
||||||
"MOTD_DISPLAY": {},
|
|
||||||
"NOISY_EXCEPTIONS": False,
|
|
||||||
"PROXIES_COUNT": None,
|
"PROXIES_COUNT": None,
|
||||||
"REAL_IP_HEADER": None,
|
"REAL_IP_HEADER": None,
|
||||||
"REGISTER": True,
|
"REGISTER": True,
|
||||||
@@ -38,39 +42,21 @@ DEFAULT_CONFIG = {
|
|||||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||||
"USE_UVLOOP": _default,
|
|
||||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||||
"WEBSOCKET_PING_INTERVAL": 20,
|
"WEBSOCKET_PING_INTERVAL": 20,
|
||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
# These values will be removed from the Config object in v22.6 and moved
|
|
||||||
# to the application state
|
|
||||||
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
|
|
||||||
|
|
||||||
|
class Config(dict):
|
||||||
class DescriptorMeta(type):
|
|
||||||
def __init__(cls, *_):
|
|
||||||
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_setter(member: object):
|
|
||||||
return isdatadescriptor(member) and hasattr(member, "setter")
|
|
||||||
|
|
||||||
|
|
||||||
class Config(dict, metaclass=DescriptorMeta):
|
|
||||||
ACCESS_LOG: bool
|
ACCESS_LOG: bool
|
||||||
AUTO_EXTEND: bool
|
|
||||||
AUTO_RELOAD: bool
|
|
||||||
EVENT_AUTOREGISTER: bool
|
EVENT_AUTOREGISTER: bool
|
||||||
|
FALLBACK_ERROR_FORMAT: str
|
||||||
FORWARDED_FOR_HEADER: str
|
FORWARDED_FOR_HEADER: str
|
||||||
FORWARDED_SECRET: Optional[str]
|
FORWARDED_SECRET: Optional[str]
|
||||||
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
||||||
KEEP_ALIVE_TIMEOUT: int
|
KEEP_ALIVE_TIMEOUT: int
|
||||||
KEEP_ALIVE: bool
|
KEEP_ALIVE: bool
|
||||||
NOISY_EXCEPTIONS: bool
|
|
||||||
MOTD: bool
|
|
||||||
MOTD_DISPLAY: Dict[str, str]
|
|
||||||
PROXIES_COUNT: Optional[int]
|
PROXIES_COUNT: Optional[int]
|
||||||
REAL_IP_HEADER: Optional[str]
|
REAL_IP_HEADER: Optional[str]
|
||||||
REGISTER: bool
|
REGISTER: bool
|
||||||
@@ -81,7 +67,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
REQUEST_TIMEOUT: int
|
REQUEST_TIMEOUT: int
|
||||||
RESPONSE_TIMEOUT: int
|
RESPONSE_TIMEOUT: int
|
||||||
SERVER_NAME: str
|
SERVER_NAME: str
|
||||||
USE_UVLOOP: Union[Default, bool]
|
|
||||||
WEBSOCKET_MAX_SIZE: int
|
WEBSOCKET_MAX_SIZE: int
|
||||||
WEBSOCKET_PING_INTERVAL: int
|
WEBSOCKET_PING_INTERVAL: int
|
||||||
WEBSOCKET_PING_TIMEOUT: int
|
WEBSOCKET_PING_TIMEOUT: int
|
||||||
@@ -89,20 +74,17 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
|
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
|
||||||
|
load_env: Optional[Union[bool, str]] = True,
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
keep_alive: Optional[bool] = None,
|
keep_alive: Optional[bool] = None,
|
||||||
*,
|
*,
|
||||||
converters: Optional[Sequence[Callable[[str], Any]]] = None,
|
app: Optional[Sanic] = None,
|
||||||
):
|
):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
|
||||||
self._converters = [str, str_to_bool, float, int]
|
self._app = app
|
||||||
self._LOGO = ""
|
self._LOGO = BASE_LOGO
|
||||||
|
|
||||||
if converters:
|
|
||||||
for converter in converters:
|
|
||||||
self.register_type(converter)
|
|
||||||
|
|
||||||
if keep_alive is not None:
|
if keep_alive is not None:
|
||||||
self.KEEP_ALIVE = keep_alive
|
self.KEEP_ALIVE = keep_alive
|
||||||
@@ -110,6 +92,15 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
if env_prefix != SANIC_PREFIX:
|
if env_prefix != SANIC_PREFIX:
|
||||||
if env_prefix:
|
if env_prefix:
|
||||||
self.load_environment_vars(env_prefix)
|
self.load_environment_vars(env_prefix)
|
||||||
|
elif load_env is not True:
|
||||||
|
if load_env:
|
||||||
|
self.load_environment_vars(prefix=load_env)
|
||||||
|
warn(
|
||||||
|
"Use of load_env is deprecated and will be removed in "
|
||||||
|
"21.12. Modify the configuration prefix by passing "
|
||||||
|
"env_prefix instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.load_environment_vars(SANIC_PREFIX)
|
self.load_environment_vars(SANIC_PREFIX)
|
||||||
|
|
||||||
@@ -130,21 +121,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.update({attr: value})
|
self.update({attr: value})
|
||||||
|
|
||||||
def update(self, *other, **kwargs) -> None:
|
def update(self, *other, **kwargs) -> None:
|
||||||
kwargs.update({k: v for item in other for k, v in dict(item).items()})
|
other_mapping = {k: v for item in other for k, v in dict(item).items()}
|
||||||
setters: Dict[str, Any] = {
|
super().update(*other, **kwargs)
|
||||||
k: kwargs.pop(k)
|
for attr, value in {**other_mapping, **kwargs}.items():
|
||||||
for k in {**kwargs}.keys()
|
|
||||||
if k in self.__class__.__setters__
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value in setters.items():
|
|
||||||
try:
|
|
||||||
super().__setattr__(key, value)
|
|
||||||
except AttributeError:
|
|
||||||
...
|
|
||||||
|
|
||||||
super().update(**kwargs)
|
|
||||||
for attr, value in {**setters, **kwargs}.items():
|
|
||||||
self._post_set(attr, value)
|
self._post_set(attr, value)
|
||||||
|
|
||||||
def _post_set(self, attr, value) -> None:
|
def _post_set(self, attr, value) -> None:
|
||||||
@@ -155,37 +134,32 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
"REQUEST_MAX_SIZE",
|
"REQUEST_MAX_SIZE",
|
||||||
):
|
):
|
||||||
self._configure_header_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":
|
elif attr == "LOGO":
|
||||||
self._LOGO = value
|
self._LOGO = value
|
||||||
deprecation(
|
warn(
|
||||||
"Setting the config.LOGO is deprecated and will no longer "
|
"Setting the config.LOGO is deprecated and will no longer "
|
||||||
"be supported starting in v22.6.",
|
"be supported starting in v22.6.",
|
||||||
22.6,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app(self):
|
||||||
|
return self._app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def LOGO(self):
|
def LOGO(self):
|
||||||
return self._LOGO
|
return self._LOGO
|
||||||
|
|
||||||
@property
|
|
||||||
def FALLBACK_ERROR_FORMAT(self) -> str:
|
|
||||||
if self._FALLBACK_ERROR_FORMAT is _default:
|
|
||||||
return DEFAULT_FORMAT
|
|
||||||
return self._FALLBACK_ERROR_FORMAT
|
|
||||||
|
|
||||||
@FALLBACK_ERROR_FORMAT.setter
|
|
||||||
def FALLBACK_ERROR_FORMAT(self, value):
|
|
||||||
self._check_error_format(value)
|
|
||||||
if (
|
|
||||||
self._FALLBACK_ERROR_FORMAT is not _default
|
|
||||||
and value != self._FALLBACK_ERROR_FORMAT
|
|
||||||
):
|
|
||||||
error_logger.warning(
|
|
||||||
"Setting config.FALLBACK_ERROR_FORMAT on an already "
|
|
||||||
"configured value may have unintended consequences."
|
|
||||||
)
|
|
||||||
self._FALLBACK_ERROR_FORMAT = value
|
|
||||||
|
|
||||||
def _configure_header_size(self):
|
def _configure_header_size(self):
|
||||||
Http.set_header_max_size(
|
Http.set_header_max_size(
|
||||||
self.REQUEST_MAX_HEADER_SIZE,
|
self.REQUEST_MAX_HEADER_SIZE,
|
||||||
@@ -193,51 +167,36 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.REQUEST_MAX_SIZE,
|
self.REQUEST_MAX_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_error_format(self, format: Optional[str] = None):
|
def _check_error_format(self):
|
||||||
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
|
check_error_format(self.FALLBACK_ERROR_FORMAT)
|
||||||
|
|
||||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||||
"""
|
"""
|
||||||
Looks for prefixed environment variables and applies them to the
|
Looks for prefixed environment variables and applies
|
||||||
configuration if present. This is called automatically when Sanic
|
them to the configuration if present. This is called automatically when
|
||||||
starts up to load environment variables into config.
|
Sanic starts up to load environment variables into config.
|
||||||
|
|
||||||
It will automatically hydrate the following types:
|
It will automatically hyrdate the following types:
|
||||||
|
|
||||||
- ``int``
|
- ``int``
|
||||||
- ``float``
|
- ``float``
|
||||||
- ``bool``
|
- ``bool``
|
||||||
|
|
||||||
Anything else will be imported as a ``str``. If you would like to add
|
Anything else will be imported as a ``str``.
|
||||||
additional types to this list, you can use
|
|
||||||
:meth:`sanic.config.Config.register_type`. Just make sure that they
|
|
||||||
are registered before you instantiate your application.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Foo:
|
|
||||||
def __init__(self, name) -> None:
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
|
|
||||||
config = Config(converters=[Foo])
|
|
||||||
app = Sanic(__name__, config=config)
|
|
||||||
|
|
||||||
`See user guide re: config
|
|
||||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
|
||||||
"""
|
"""
|
||||||
for key, value in environ.items():
|
for k, v in environ.items():
|
||||||
if not key.startswith(prefix):
|
if k.startswith(prefix):
|
||||||
continue
|
_, config_key = k.split(prefix, 1)
|
||||||
|
|
||||||
_, config_key = key.split(prefix, 1)
|
|
||||||
|
|
||||||
for converter in reversed(self._converters):
|
|
||||||
try:
|
try:
|
||||||
self[config_key] = converter(value)
|
self[config_key] = int(v)
|
||||||
break
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
try:
|
||||||
|
self[config_key] = float(v)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
self[config_key] = str_to_bool(v)
|
||||||
|
except ValueError:
|
||||||
|
self[config_key] = v
|
||||||
|
|
||||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""
|
"""
|
||||||
@@ -307,17 +266,3 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.update(config)
|
self.update(config)
|
||||||
|
|
||||||
load = update_config
|
load = update_config
|
||||||
|
|
||||||
def register_type(self, converter: Callable[[str], Any]) -> None:
|
|
||||||
"""
|
|
||||||
Allows for adding custom function to cast from a string value to any
|
|
||||||
other type. The function should raise ValueError if it is not the
|
|
||||||
correct type.
|
|
||||||
"""
|
|
||||||
if converter in self._converters:
|
|
||||||
error_logger.warning(
|
|
||||||
f"Configuration value converter '{converter.__name__}' has "
|
|
||||||
"already been registered"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
self._converters.append(converter)
|
|
||||||
|
|||||||
@@ -25,16 +25,14 @@ from sanic.request import Request
|
|||||||
from sanic.response import HTTPResponse, html, json, text
|
from sanic.response import HTTPResponse, html, json, text
|
||||||
|
|
||||||
|
|
||||||
dumps: t.Callable[..., str]
|
|
||||||
try:
|
try:
|
||||||
from ujson import dumps
|
from ujson import dumps
|
||||||
|
|
||||||
dumps = partial(dumps, escape_forward_slashes=False)
|
dumps = partial(dumps, escape_forward_slashes=False)
|
||||||
except ImportError: # noqa
|
except ImportError: # noqa
|
||||||
from json import dumps
|
from json import dumps # type: ignore
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_FORMAT = "auto"
|
|
||||||
FALLBACK_TEXT = (
|
FALLBACK_TEXT = (
|
||||||
"The server encountered an internal error and "
|
"The server encountered an internal error and "
|
||||||
"cannot complete your request."
|
"cannot complete your request."
|
||||||
@@ -47,8 +45,6 @@ class BaseRenderer:
|
|||||||
Base class that all renderers must inherit from.
|
Base class that all renderers must inherit from.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dumps = staticmethod(dumps)
|
|
||||||
|
|
||||||
def __init__(self, request, exception, debug):
|
def __init__(self, request, exception, debug):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.exception = exception
|
self.exception = exception
|
||||||
@@ -116,16 +112,14 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
TRACEBACK_STYLE = """
|
TRACEBACK_STYLE = """
|
||||||
html { font-family: sans-serif }
|
html { font-family: sans-serif }
|
||||||
h2 { color: #888; }
|
h2 { color: #888; }
|
||||||
.tb-wrapper p, dl, dd { margin: 0 }
|
.tb-wrapper p { margin: 0 }
|
||||||
.frame-border { margin: 1rem }
|
.frame-border { margin: 1rem }
|
||||||
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
|
.frame-line > * { padding: 0.3rem 0.6rem }
|
||||||
.frame-line, dl { margin-bottom: 0.3rem }
|
.frame-line { margin-bottom: 0.3rem }
|
||||||
.frame-code, dd { font-size: 16px; padding-left: 4ch }
|
.frame-code { font-size: 16px; padding-left: 4ch }
|
||||||
.tb-wrapper, dl { border: 1px solid #eee }
|
.tb-wrapper { border: 1px solid #eee }
|
||||||
.tb-header,.obj-header {
|
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
|
||||||
background: #eee; padding: 0.3rem; font-weight: bold
|
.frame-descriptor { background: #e2eafb; font-size: 14px }
|
||||||
}
|
|
||||||
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
|
|
||||||
"""
|
"""
|
||||||
TRACEBACK_WRAPPER_HTML = (
|
TRACEBACK_WRAPPER_HTML = (
|
||||||
"<div class=tb-header>{exc_name}: {exc_value}</div>"
|
"<div class=tb-header>{exc_name}: {exc_value}</div>"
|
||||||
@@ -144,11 +138,6 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
"<p class=frame-code><code>{0.line}</code>"
|
"<p class=frame-code><code>{0.line}</code>"
|
||||||
"</div>"
|
"</div>"
|
||||||
)
|
)
|
||||||
OBJECT_WRAPPER_HTML = (
|
|
||||||
"<div class=obj-header>{title}</div>"
|
|
||||||
"<dl class={obj_type}>{display_html}</dl>"
|
|
||||||
)
|
|
||||||
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
|
|
||||||
OUTPUT_HTML = (
|
OUTPUT_HTML = (
|
||||||
"<!DOCTYPE html><html lang=en>"
|
"<!DOCTYPE html><html lang=en>"
|
||||||
"<meta charset=UTF-8><title>{title}</title>\n"
|
"<meta charset=UTF-8><title>{title}</title>\n"
|
||||||
@@ -163,7 +152,7 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
title=self.title,
|
title=self.title,
|
||||||
text=self.text,
|
text=self.text,
|
||||||
style=self.TRACEBACK_STYLE,
|
style=self.TRACEBACK_STYLE,
|
||||||
body=self._generate_body(full=True),
|
body=self._generate_body(),
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
)
|
)
|
||||||
@@ -174,7 +163,7 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
title=self.title,
|
title=self.title,
|
||||||
text=self.text,
|
text=self.text,
|
||||||
style=self.TRACEBACK_STYLE,
|
style=self.TRACEBACK_STYLE,
|
||||||
body=self._generate_body(full=False),
|
body="",
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
@@ -188,49 +177,27 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
def title(self):
|
def title(self):
|
||||||
return escape(f"⚠️ {super().title}")
|
return escape(f"⚠️ {super().title}")
|
||||||
|
|
||||||
def _generate_body(self, *, full):
|
def _generate_body(self):
|
||||||
lines = []
|
_, exc_value, __ = sys.exc_info()
|
||||||
if full:
|
exceptions = []
|
||||||
_, exc_value, __ = sys.exc_info()
|
while exc_value:
|
||||||
exceptions = []
|
exceptions.append(self._format_exc(exc_value))
|
||||||
while exc_value:
|
exc_value = exc_value.__cause__
|
||||||
exceptions.append(self._format_exc(exc_value))
|
|
||||||
exc_value = exc_value.__cause__
|
|
||||||
|
|
||||||
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
|
||||||
appname = escape(self.request.app.name)
|
|
||||||
name = escape(self.exception.__class__.__name__)
|
|
||||||
value = escape(self.exception)
|
|
||||||
path = escape(self.request.path)
|
|
||||||
lines += [
|
|
||||||
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
|
|
||||||
f"{traceback_html}",
|
|
||||||
"<div class=summary><p>",
|
|
||||||
f"<b>{name}: {value}</b> "
|
|
||||||
f"while handling path <code>{path}</code>",
|
|
||||||
"</div>",
|
|
||||||
]
|
|
||||||
|
|
||||||
for attr, display in (("context", True), ("extra", bool(full))):
|
|
||||||
info = getattr(self.exception, attr, None)
|
|
||||||
if info and display:
|
|
||||||
lines.append(self._generate_object_display(info, attr))
|
|
||||||
|
|
||||||
|
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
|
||||||
|
appname = escape(self.request.app.name)
|
||||||
|
name = escape(self.exception.__class__.__name__)
|
||||||
|
value = escape(self.exception)
|
||||||
|
path = escape(self.request.path)
|
||||||
|
lines = [
|
||||||
|
f"<h2>Traceback of {appname} (most recent call last):</h2>",
|
||||||
|
f"{traceback_html}",
|
||||||
|
"<div class=summary><p>",
|
||||||
|
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
|
||||||
|
"</div>",
|
||||||
|
]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _generate_object_display(
|
|
||||||
self, obj: t.Dict[str, t.Any], descriptor: str
|
|
||||||
) -> str:
|
|
||||||
display = "".join(
|
|
||||||
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
|
|
||||||
for key, value in obj.items()
|
|
||||||
)
|
|
||||||
return self.OBJECT_WRAPPER_HTML.format(
|
|
||||||
title=descriptor.title(),
|
|
||||||
display_html=display,
|
|
||||||
obj_type=descriptor.lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _format_exc(self, exc):
|
def _format_exc(self, exc):
|
||||||
frames = extract_tb(exc.__traceback__)
|
frames = extract_tb(exc.__traceback__)
|
||||||
frame_html = "".join(
|
frame_html = "".join(
|
||||||
@@ -257,7 +224,7 @@ class TextRenderer(BaseRenderer):
|
|||||||
title=self.title,
|
title=self.title,
|
||||||
text=self.text,
|
text=self.text,
|
||||||
bar=("=" * len(self.title)),
|
bar=("=" * len(self.title)),
|
||||||
body=self._generate_body(full=True),
|
body=self._generate_body(),
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
)
|
)
|
||||||
@@ -268,7 +235,7 @@ class TextRenderer(BaseRenderer):
|
|||||||
title=self.title,
|
title=self.title,
|
||||||
text=self.text,
|
text=self.text,
|
||||||
bar=("=" * len(self.title)),
|
bar=("=" * len(self.title)),
|
||||||
body=self._generate_body(full=False),
|
body="",
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
@@ -278,31 +245,21 @@ class TextRenderer(BaseRenderer):
|
|||||||
def title(self):
|
def title(self):
|
||||||
return f"⚠️ {super().title}"
|
return f"⚠️ {super().title}"
|
||||||
|
|
||||||
def _generate_body(self, *, full):
|
def _generate_body(self):
|
||||||
lines = []
|
_, exc_value, __ = sys.exc_info()
|
||||||
if full:
|
exceptions = []
|
||||||
_, exc_value, __ = sys.exc_info()
|
|
||||||
exceptions = []
|
|
||||||
|
|
||||||
lines += [
|
lines = [
|
||||||
f"{self.exception.__class__.__name__}: {self.exception} while "
|
f"{self.exception.__class__.__name__}: {self.exception} while "
|
||||||
f"handling path {self.request.path}",
|
f"handling path {self.request.path}",
|
||||||
f"Traceback of {self.request.app.name} "
|
f"Traceback of {self.request.app.name} (most recent call last):\n",
|
||||||
"(most recent call last):\n",
|
]
|
||||||
]
|
|
||||||
|
|
||||||
while exc_value:
|
while exc_value:
|
||||||
exceptions.append(self._format_exc(exc_value))
|
exceptions.append(self._format_exc(exc_value))
|
||||||
exc_value = exc_value.__cause__
|
exc_value = exc_value.__cause__
|
||||||
|
|
||||||
lines += exceptions[::-1]
|
return "\n".join(lines + exceptions[::-1])
|
||||||
|
|
||||||
for attr, display in (("context", True), ("extra", bool(full))):
|
|
||||||
info = getattr(self.exception, attr, None)
|
|
||||||
if info and display:
|
|
||||||
lines += self._generate_object_display_list(info, attr)
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def _format_exc(self, exc):
|
def _format_exc(self, exc):
|
||||||
frames = "\n\n".join(
|
frames = "\n\n".join(
|
||||||
@@ -315,13 +272,6 @@ class TextRenderer(BaseRenderer):
|
|||||||
)
|
)
|
||||||
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
|
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
|
||||||
|
|
||||||
def _generate_object_display_list(self, obj, descriptor):
|
|
||||||
lines = [f"\n{descriptor.title()}"]
|
|
||||||
for key, value in obj.items():
|
|
||||||
display = self.dumps(value)
|
|
||||||
lines.append(f"{self.SPACER * 2}{key}: {display}")
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
class JSONRenderer(BaseRenderer):
|
class JSONRenderer(BaseRenderer):
|
||||||
"""
|
"""
|
||||||
@@ -330,11 +280,11 @@ class JSONRenderer(BaseRenderer):
|
|||||||
|
|
||||||
def full(self) -> HTTPResponse:
|
def full(self) -> HTTPResponse:
|
||||||
output = self._generate_output(full=True)
|
output = self._generate_output(full=True)
|
||||||
return json(output, status=self.status, dumps=self.dumps)
|
return json(output, status=self.status, dumps=dumps)
|
||||||
|
|
||||||
def minimal(self) -> HTTPResponse:
|
def minimal(self) -> HTTPResponse:
|
||||||
output = self._generate_output(full=False)
|
output = self._generate_output(full=False)
|
||||||
return json(output, status=self.status, dumps=self.dumps)
|
return json(output, status=self.status, dumps=dumps)
|
||||||
|
|
||||||
def _generate_output(self, *, full):
|
def _generate_output(self, *, full):
|
||||||
output = {
|
output = {
|
||||||
@@ -343,11 +293,6 @@ class JSONRenderer(BaseRenderer):
|
|||||||
"message": self.text,
|
"message": self.text,
|
||||||
}
|
}
|
||||||
|
|
||||||
for attr, display in (("context", True), ("extra", bool(full))):
|
|
||||||
info = getattr(self.exception, attr, None)
|
|
||||||
if info and display:
|
|
||||||
output[attr] = info
|
|
||||||
|
|
||||||
if full:
|
if full:
|
||||||
_, exc_value, __ = sys.exc_info()
|
_, exc_value, __ = sys.exc_info()
|
||||||
exceptions = []
|
exceptions = []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Dict, Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
@@ -11,11 +11,7 @@ class SanicException(Exception):
|
|||||||
message: Optional[Union[str, bytes]] = None,
|
message: Optional[Union[str, bytes]] = None,
|
||||||
status_code: Optional[int] = None,
|
status_code: Optional[int] = None,
|
||||||
quiet: Optional[bool] = None,
|
quiet: Optional[bool] = None,
|
||||||
context: Optional[Dict[str, Any]] = None,
|
|
||||||
extra: Optional[Dict[str, Any]] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.context = context
|
|
||||||
self.extra = extra
|
|
||||||
if message is None:
|
if message is None:
|
||||||
if self.message:
|
if self.message:
|
||||||
message = self.message
|
message = self.message
|
||||||
@@ -244,3 +240,25 @@ class InvalidSignal(SanicException):
|
|||||||
class WebsocketClosed(SanicException):
|
class WebsocketClosed(SanicException):
|
||||||
quiet = True
|
quiet = True
|
||||||
message = "Client has closed the websocket connection"
|
message = "Client has closed the websocket connection"
|
||||||
|
|
||||||
|
|
||||||
|
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
|
||||||
|
"""
|
||||||
|
Raise an exception based on SanicException. Returns the HTTP response
|
||||||
|
message appropriate for the given status code, unless provided.
|
||||||
|
|
||||||
|
STATUS_CODES from sanic.helpers for the given status code.
|
||||||
|
|
||||||
|
:param status_code: The HTTP status code to return.
|
||||||
|
:param message: The HTTP response body. Defaults to the messages in
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"sanic.exceptions.abort has been marked as deprecated, and will be "
|
||||||
|
"removed in release 21.12.\n To migrate your code, simply replace "
|
||||||
|
"abort(status_code, msg) with raise SanicException(msg, status_code), "
|
||||||
|
"or even better, raise an appropriate SanicException subclass."
|
||||||
|
)
|
||||||
|
|
||||||
|
raise SanicException(message=message, status_code=status_code)
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from typing import Dict, List, Optional, Tuple, Type, Union
|
from typing import Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
from sanic.config import Config
|
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
|
||||||
from sanic.errorpages import (
|
|
||||||
DEFAULT_FORMAT,
|
|
||||||
BaseRenderer,
|
|
||||||
HTMLRenderer,
|
|
||||||
exception_response,
|
|
||||||
)
|
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
ContentRangeError,
|
ContentRangeError,
|
||||||
HeaderNotFound,
|
HeaderNotFound,
|
||||||
InvalidRangeType,
|
InvalidRangeType,
|
||||||
SanicException,
|
|
||||||
)
|
)
|
||||||
from sanic.helpers import Default, _default
|
from sanic.log import error_logger
|
||||||
from sanic.log import deprecation, error_logger
|
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@@ -37,91 +27,24 @@ class ErrorHandler:
|
|||||||
|
|
||||||
# Beginning in v22.3, the base renderer will be TextRenderer
|
# Beginning in v22.3, the base renderer will be TextRenderer
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer
|
||||||
fallback: Union[str, Default] = _default,
|
|
||||||
base: Type[BaseRenderer] = HTMLRenderer,
|
|
||||||
):
|
):
|
||||||
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
|
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
|
||||||
self.cached_handlers: Dict[
|
self.cached_handlers: Dict[
|
||||||
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
||||||
] = {}
|
] = {}
|
||||||
self.debug = False
|
self.debug = False
|
||||||
self._fallback = fallback
|
self.fallback = fallback
|
||||||
self.base = base
|
self.base = base
|
||||||
|
|
||||||
if fallback is not _default:
|
|
||||||
self._warn_fallback_deprecation()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fallback(self):
|
|
||||||
# This is for backwards compat and can be removed in v22.6
|
|
||||||
if self._fallback is _default:
|
|
||||||
return DEFAULT_FORMAT
|
|
||||||
return self._fallback
|
|
||||||
|
|
||||||
@fallback.setter
|
|
||||||
def fallback(self, value: str):
|
|
||||||
self._warn_fallback_deprecation()
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise SanicException(
|
|
||||||
f"Cannot set error handler fallback to: value={value}"
|
|
||||||
)
|
|
||||||
self._fallback = value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _warn_fallback_deprecation():
|
|
||||||
deprecation(
|
|
||||||
"Setting the ErrorHandler fallback value directly is "
|
|
||||||
"deprecated and no longer supported. This feature will "
|
|
||||||
"be removed in v22.6. Instead, use "
|
|
||||||
"app.config.FALLBACK_ERROR_FORMAT.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
|
def finalize(cls, error_handler, fallback: Optional[str] = None):
|
||||||
if error_handler._fallback is not _default:
|
if (
|
||||||
if config._FALLBACK_ERROR_FORMAT is _default:
|
fallback
|
||||||
return error_handler.fallback
|
and fallback != "auto"
|
||||||
|
and error_handler.fallback == "auto"
|
||||||
error_logger.warning(
|
):
|
||||||
"Conflicting error fallback values were found in the "
|
error_handler.fallback = fallback
|
||||||
"error handler and in the app.config while handling an "
|
|
||||||
"exception. Using the value from app.config."
|
|
||||||
)
|
|
||||||
return config.FALLBACK_ERROR_FORMAT
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def finalize(
|
|
||||||
cls,
|
|
||||||
error_handler: ErrorHandler,
|
|
||||||
fallback: Optional[str] = None,
|
|
||||||
config: Optional[Config] = None,
|
|
||||||
):
|
|
||||||
if fallback:
|
|
||||||
deprecation(
|
|
||||||
"Setting the ErrorHandler fallback value via finalize() "
|
|
||||||
"is deprecated and no longer supported. This feature will "
|
|
||||||
"be removed in v22.6. Instead, use "
|
|
||||||
"app.config.FALLBACK_ERROR_FORMAT.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
|
|
||||||
if config is None:
|
|
||||||
deprecation(
|
|
||||||
"Starting in v22.3, config will be a required argument "
|
|
||||||
"for ErrorHandler.finalize().",
|
|
||||||
22.3,
|
|
||||||
)
|
|
||||||
|
|
||||||
if fallback and fallback != DEFAULT_FORMAT:
|
|
||||||
if error_handler._fallback is not _default:
|
|
||||||
error_logger.warning(
|
|
||||||
f"Setting the fallback value to {fallback}. This changes "
|
|
||||||
"the current non-default value "
|
|
||||||
f"'{error_handler._fallback}'."
|
|
||||||
)
|
|
||||||
error_handler._fallback = fallback
|
|
||||||
|
|
||||||
if not isinstance(error_handler, cls):
|
if not isinstance(error_handler, cls):
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
@@ -130,18 +53,18 @@ class ErrorHandler:
|
|||||||
|
|
||||||
sig = signature(error_handler.lookup)
|
sig = signature(error_handler.lookup)
|
||||||
if len(sig.parameters) == 1:
|
if len(sig.parameters) == 1:
|
||||||
deprecation(
|
error_logger.warning(
|
||||||
"You are using a deprecated error handler. The lookup "
|
DeprecationWarning(
|
||||||
"method should accept two positional parameters: "
|
"You are using a deprecated error handler. The lookup "
|
||||||
"(exception, route_name: Optional[str]). "
|
"method should accept two positional parameters: "
|
||||||
"Until you upgrade your ErrorHandler.lookup, Blueprint "
|
"(exception, route_name: Optional[str]). "
|
||||||
"specific exceptions will not work properly. Beginning "
|
"Until you upgrade your ErrorHandler.lookup, Blueprint "
|
||||||
"in v22.3, the legacy style lookup method will not "
|
"specific exceptions will not work properly. Beginning "
|
||||||
"work at all.",
|
"in v22.3, the legacy style lookup method will not "
|
||||||
22.3,
|
"work at all."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
legacy_lookup = error_handler._legacy_lookup
|
error_handler._lookup = error_handler._legacy_lookup
|
||||||
error_handler._lookup = legacy_lookup # type: ignore
|
|
||||||
|
|
||||||
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
||||||
return self.lookup(exception, route_name)
|
return self.lookup(exception, route_name)
|
||||||
@@ -265,20 +188,18 @@ class ErrorHandler:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self.log(request, exception)
|
self.log(request, exception)
|
||||||
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
|
|
||||||
return exception_response(
|
return exception_response(
|
||||||
request,
|
request,
|
||||||
exception,
|
exception,
|
||||||
debug=self.debug,
|
debug=self.debug,
|
||||||
base=self.base,
|
base=self.base,
|
||||||
fallback=fallback,
|
fallback=self.fallback,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def log(request, exception):
|
def log(request, exception):
|
||||||
quiet = getattr(exception, "quiet", False)
|
quiet = getattr(exception, "quiet", False)
|
||||||
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
|
if quiet is False:
|
||||||
if quiet is False or noisy is True:
|
|
||||||
try:
|
try:
|
||||||
url = repr(request.url)
|
url = repr(request.url)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ _host_re = re.compile(
|
|||||||
|
|
||||||
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
|
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
|
||||||
# curl all have different escaping, that we try to handle as well as possible,
|
# curl all have different escaping, that we try to handle as well as possible,
|
||||||
# even though no client escapes in a way that would allow perfect handling.
|
# even though no client espaces in a way that would allow perfect handling.
|
||||||
|
|
||||||
# For more information, consult ../tests/test_requests.py
|
# For more information, consult ../tests/test_requests.py
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ def import_string(module_name, package=None):
|
|||||||
import a module or class by string path.
|
import a module or class by string path.
|
||||||
|
|
||||||
:module_name: str with path of module or path to import and
|
:module_name: str with path of module or path to import and
|
||||||
instantiate a class
|
instanciate a class
|
||||||
:returns: a module object or one instance from class if
|
:returns: a module object or one instance from class if
|
||||||
module_name is a valid path to class
|
module_name is a valid path to class
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import BaseHTTPResponse
|
from sanic.response import BaseHTTPResponse
|
||||||
|
|
||||||
@@ -584,11 +584,6 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
self.stage = Stage.FAILED
|
self.stage = Stage.FAILED
|
||||||
raise RuntimeError("Response already started")
|
raise RuntimeError("Response already started")
|
||||||
|
|
||||||
# Disconnect any earlier but unused response object
|
|
||||||
if self.response is not None:
|
|
||||||
self.response.stream = None
|
|
||||||
|
|
||||||
# Connect and return the response
|
|
||||||
self.response, response.stream = response, self
|
self.response, response.stream = response, self
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
22
sanic/log.py
22
sanic/log.py
@@ -1,12 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, Dict
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
|
LOGGING_CONFIG_DEFAULTS = dict(
|
||||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
|
|
||||||
version=1,
|
version=1,
|
||||||
disable_existing_loggers=False,
|
disable_existing_loggers=False,
|
||||||
loggers={
|
loggers={
|
||||||
@@ -57,14 +53,6 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Colors(str, Enum):
|
|
||||||
END = "\033[0m"
|
|
||||||
BLUE = "\033[01;34m"
|
|
||||||
GREEN = "\033[01;32m"
|
|
||||||
YELLOW = "\033[01;33m"
|
|
||||||
RED = "\033[01;31m"
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("sanic.root")
|
logger = logging.getLogger("sanic.root")
|
||||||
"""
|
"""
|
||||||
General Sanic logger
|
General Sanic logger
|
||||||
@@ -79,11 +67,3 @@ access_logger = logging.getLogger("sanic.access")
|
|||||||
"""
|
"""
|
||||||
Logger used by Sanic for access logging
|
Logger used by Sanic for access logging
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def deprecation(message: str, version: float):
|
|
||||||
version_info = f"[DEPRECATION v{version}] "
|
|
||||||
if sys.stdout.isatty():
|
|
||||||
version_info = f"{Colors.RED}{version_info}"
|
|
||||||
message = f"{Colors.YELLOW}{message}{Colors.END}"
|
|
||||||
warn(version_info + message, DeprecationWarning)
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.models.futures import FutureException
|
from sanic.models.futures import FutureException
|
||||||
|
|
||||||
|
|
||||||
class ExceptionMixin(metaclass=SanicMeta):
|
class ExceptionMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_exceptions: Set[FutureException] = set()
|
self._future_exceptions: Set[FutureException] = set()
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ from enum import Enum, auto
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.models.futures import FutureListener
|
from sanic.models.futures import FutureListener
|
||||||
from sanic.models.handler_types import ListenerType, Sanic
|
from sanic.models.handler_types import ListenerType
|
||||||
|
|
||||||
|
|
||||||
class ListenerEvent(str, Enum):
|
class ListenerEvent(str, Enum):
|
||||||
@@ -19,7 +18,7 @@ class ListenerEvent(str, Enum):
|
|||||||
MAIN_PROCESS_STOP = auto()
|
MAIN_PROCESS_STOP = auto()
|
||||||
|
|
||||||
|
|
||||||
class ListenerMixin(metaclass=SanicMeta):
|
class ListenerMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_listeners: List[FutureListener] = []
|
self._future_listeners: List[FutureListener] = []
|
||||||
|
|
||||||
@@ -28,10 +27,10 @@ class ListenerMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
def listener(
|
def listener(
|
||||||
self,
|
self,
|
||||||
listener_or_event: Union[ListenerType[Sanic], str],
|
listener_or_event: Union[ListenerType, str],
|
||||||
event_or_none: Optional[str] = None,
|
event_or_none: Optional[str] = None,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
) -> ListenerType[Sanic]:
|
):
|
||||||
"""
|
"""
|
||||||
Create a listener from a decorated function.
|
Create a listener from a decorated function.
|
||||||
|
|
||||||
@@ -63,32 +62,20 @@ class ListenerMixin(metaclass=SanicMeta):
|
|||||||
else:
|
else:
|
||||||
return partial(register_listener, event=listener_or_event)
|
return partial(register_listener, event=listener_or_event)
|
||||||
|
|
||||||
def main_process_start(
|
def main_process_start(self, listener: ListenerType) -> ListenerType:
|
||||||
self, listener: ListenerType[Sanic]
|
|
||||||
) -> ListenerType[Sanic]:
|
|
||||||
return self.listener(listener, "main_process_start")
|
return self.listener(listener, "main_process_start")
|
||||||
|
|
||||||
def main_process_stop(
|
def main_process_stop(self, listener: ListenerType) -> ListenerType:
|
||||||
self, listener: ListenerType[Sanic]
|
|
||||||
) -> ListenerType[Sanic]:
|
|
||||||
return self.listener(listener, "main_process_stop")
|
return self.listener(listener, "main_process_stop")
|
||||||
|
|
||||||
def before_server_start(
|
def before_server_start(self, listener: ListenerType) -> ListenerType:
|
||||||
self, listener: ListenerType[Sanic]
|
|
||||||
) -> ListenerType[Sanic]:
|
|
||||||
return self.listener(listener, "before_server_start")
|
return self.listener(listener, "before_server_start")
|
||||||
|
|
||||||
def after_server_start(
|
def after_server_start(self, listener: ListenerType) -> ListenerType:
|
||||||
self, listener: ListenerType[Sanic]
|
|
||||||
) -> ListenerType[Sanic]:
|
|
||||||
return self.listener(listener, "after_server_start")
|
return self.listener(listener, "after_server_start")
|
||||||
|
|
||||||
def before_server_stop(
|
def before_server_stop(self, listener: ListenerType) -> ListenerType:
|
||||||
self, listener: ListenerType[Sanic]
|
|
||||||
) -> ListenerType[Sanic]:
|
|
||||||
return self.listener(listener, "before_server_stop")
|
return self.listener(listener, "before_server_stop")
|
||||||
|
|
||||||
def after_server_stop(
|
def after_server_stop(self, listener: ListenerType) -> ListenerType:
|
||||||
self, listener: ListenerType[Sanic]
|
|
||||||
) -> ListenerType[Sanic]:
|
|
||||||
return self.listener(listener, "after_server_stop")
|
return self.listener(listener, "after_server_stop")
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.models.futures import FutureMiddleware
|
from sanic.models.futures import FutureMiddleware
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareMixin(metaclass=SanicMeta):
|
class MiddlewareMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_middleware: List[FutureMiddleware] = []
|
self._future_middleware: List[FutureMiddleware] = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from ast import NodeVisitor, Return, parse
|
from ast import NodeVisitor, Return, parse
|
||||||
from contextlib import suppress
|
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from inspect import getsource, signature
|
from inspect import getsource, signature
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
@@ -13,7 +12,6 @@ from urllib.parse import unquote
|
|||||||
|
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
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.errorpages import RESPONSE_MAPPING
|
||||||
@@ -24,27 +22,19 @@ from sanic.exceptions import (
|
|||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
)
|
)
|
||||||
from sanic.handlers import ContentRangeHandler
|
from sanic.handlers import ContentRangeHandler
|
||||||
from sanic.log import deprecation, 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.models.handler_types import RouteHandler
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
from sanic.response import HTTPResponse, file, file_stream
|
||||||
from sanic.types import HashableDict
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
RouteWrapper = Callable[
|
RouteWrapper = Callable[
|
||||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||||
]
|
]
|
||||||
RESTRICTED_ROUTE_CONTEXT = (
|
|
||||||
"ignore_body",
|
|
||||||
"stream",
|
|
||||||
"hosts",
|
|
||||||
"static",
|
|
||||||
"error_format",
|
|
||||||
"websocket",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin(metaclass=SanicMeta):
|
class RouteMixin:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
@@ -62,7 +52,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
methods: Optional[Iterable[str]] = None,
|
methods: Optional[Iterable[str]] = None,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
@@ -75,20 +65,10 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
static: bool = False,
|
static: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs: Any,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a route
|
Decorate a function to be registered as a route
|
||||||
|
|
||||||
|
|
||||||
**Example using context kwargs**
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@app.route(..., ctx_foo="foobar")
|
|
||||||
async def route_handler(request: Request):
|
|
||||||
assert request.route.ctx.foo == "foobar"
|
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
:param methods: list or tuple of methods allowed
|
:param methods: list or tuple of methods allowed
|
||||||
:param host: the host, if required
|
:param host: the host, if required
|
||||||
@@ -100,8 +80,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
body (eg. GET requests)
|
body (eg. GET requests)
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -116,8 +94,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
if not methods and not websocket:
|
if not methods and not websocket:
|
||||||
methods = frozenset({"GET"})
|
methods = frozenset({"GET"})
|
||||||
|
|
||||||
route_context = self._build_route_context(ctx_kwargs)
|
|
||||||
|
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
nonlocal uri
|
nonlocal uri
|
||||||
nonlocal methods
|
nonlocal methods
|
||||||
@@ -176,7 +152,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
static,
|
static,
|
||||||
version_prefix,
|
version_prefix,
|
||||||
error_format,
|
error_format,
|
||||||
route_context,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._future_routes.add(route)
|
self._future_routes.add(route)
|
||||||
@@ -214,14 +189,13 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
handler: RouteHandler,
|
handler: RouteHandler,
|
||||||
uri: str,
|
uri: str,
|
||||||
methods: Iterable[str] = frozenset({"GET"}),
|
methods: Iterable[str] = frozenset({"GET"}),
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteHandler:
|
) -> 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
|
||||||
@@ -238,8 +212,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
: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
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
@@ -254,6 +226,14 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
if hasattr(_handler, "is_stream"):
|
if hasattr(_handler, "is_stream"):
|
||||||
stream = True
|
stream = True
|
||||||
|
|
||||||
|
# handle composition view differently
|
||||||
|
if isinstance(handler, CompositionView):
|
||||||
|
methods = handler.handlers.keys()
|
||||||
|
for _handler in handler.handlers.values():
|
||||||
|
if hasattr(_handler, "is_stream"):
|
||||||
|
stream = True
|
||||||
|
break
|
||||||
|
|
||||||
if strict_slashes is None:
|
if strict_slashes is None:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
@@ -267,7 +247,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)(handler)
|
)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
@@ -275,14 +254,13 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
def get(
|
def get(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[Union[int, str, float]] = 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",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **GET** *HTTP* method
|
Add an API URL under the **GET** *HTTP* method
|
||||||
@@ -295,8 +273,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
: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
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -309,20 +285,18 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(
|
def post(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **POST** *HTTP* method
|
Add an API URL under the **POST** *HTTP* method
|
||||||
@@ -335,8 +309,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
: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
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -349,20 +321,18 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def put(
|
def put(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PUT** *HTTP* method
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
@@ -375,8 +345,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
: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
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -389,20 +357,18 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def head(
|
def head(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[Union[int, str, float]] = 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",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **HEAD** *HTTP* method
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
@@ -423,8 +389,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -437,20 +401,18 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def options(
|
def options(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[Union[int, str, float]] = 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",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **OPTIONS** *HTTP* method
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
@@ -471,8 +433,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -485,20 +445,18 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(
|
def patch(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream=False,
|
stream=False,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PATCH** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
@@ -521,8 +479,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
:type ignore_body: bool, optional
|
:type ignore_body: bool, optional
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -535,20 +491,18 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[Union[int, str, float]] = 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",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
) -> RouteWrapper:
|
) -> RouteWrapper:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
@@ -561,8 +515,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
: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
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -575,21 +527,19 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body=ignore_body,
|
ignore_body=ignore_body,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def websocket(
|
def websocket(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
subprotocols: Optional[List[str]] = None,
|
subprotocols: Optional[List[str]] = None,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a websocket route
|
Decorate a function to be registered as a websocket route
|
||||||
@@ -603,8 +553,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
be used with :func:`url_for`
|
be used with :func:`url_for`
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: tuple of routes, decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
@@ -619,21 +567,19 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
websocket=True,
|
websocket=True,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_websocket_route(
|
def add_websocket_route(
|
||||||
self,
|
self,
|
||||||
handler,
|
handler,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[Union[str, List[str]]] = None,
|
host: Optional[str] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
subprotocols=None,
|
subprotocols=None,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
A helper method to register a function as a websocket route.
|
A helper method to register a function as a websocket route.
|
||||||
@@ -652,8 +598,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
be used with :func:`url_for`
|
be used with :func:`url_for`
|
||||||
:param version_prefix: URL path that should be before the version
|
:param version_prefix: URL path that should be before the version
|
||||||
value; default: ``/v``
|
value; default: ``/v``
|
||||||
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
|
|
||||||
will be appended to the route context (``route.ctx``)
|
|
||||||
:return: Objected decorated by :func:`websocket`
|
:return: Objected decorated by :func:`websocket`
|
||||||
"""
|
"""
|
||||||
return self.websocket(
|
return self.websocket(
|
||||||
@@ -665,7 +609,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
**ctx_kwargs,
|
|
||||||
)(handler)
|
)(handler)
|
||||||
|
|
||||||
def static(
|
def static(
|
||||||
@@ -975,16 +918,19 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
return route
|
return route
|
||||||
|
|
||||||
def _determine_error_format(self, handler) -> str:
|
def _determine_error_format(self, handler) -> Optional[str]:
|
||||||
with suppress(OSError, TypeError):
|
if not isinstance(handler, CompositionView):
|
||||||
src = dedent(getsource(handler))
|
try:
|
||||||
tree = parse(src)
|
src = dedent(getsource(handler))
|
||||||
http_response_types = self._get_response_types(tree)
|
tree = parse(src)
|
||||||
|
http_response_types = self._get_response_types(tree)
|
||||||
|
|
||||||
if len(http_response_types) == 1:
|
if len(http_response_types) == 1:
|
||||||
return next(iter(http_response_types))
|
return next(iter(http_response_types))
|
||||||
|
except (OSError, TypeError):
|
||||||
|
...
|
||||||
|
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
def _get_response_types(self, node):
|
def _get_response_types(self, node):
|
||||||
types = set()
|
types = set()
|
||||||
@@ -993,18 +939,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
def visit_Return(self, node: Return) -> Any:
|
def visit_Return(self, node: Return) -> Any:
|
||||||
nonlocal types
|
nonlocal types
|
||||||
|
|
||||||
with suppress(AttributeError):
|
try:
|
||||||
if node.value.func.id == "stream": # type: ignore
|
|
||||||
deprecation(
|
|
||||||
"The sanic.response.stream method has been "
|
|
||||||
"deprecated and will be removed in v22.6. Please "
|
|
||||||
"upgrade your application to use the new style "
|
|
||||||
"streaming pattern. See "
|
|
||||||
"https://sanicframework.org/en/guide/advanced/"
|
|
||||||
"streaming.html#response-streaming for more "
|
|
||||||
"information.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
checks = [node.value.func.id] # type: ignore
|
checks = [node.value.func.id] # type: ignore
|
||||||
if node.value.keywords: # type: ignore
|
if node.value.keywords: # type: ignore
|
||||||
checks += [
|
checks += [
|
||||||
@@ -1016,32 +951,9 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
for check in checks:
|
for check in checks:
|
||||||
if check in RESPONSE_MAPPING:
|
if check in RESPONSE_MAPPING:
|
||||||
types.add(RESPONSE_MAPPING[check])
|
types.add(RESPONSE_MAPPING[check])
|
||||||
|
except AttributeError:
|
||||||
|
...
|
||||||
|
|
||||||
HttpResponseVisitor().visit(node)
|
HttpResponseVisitor().visit(node)
|
||||||
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
def _build_route_context(self, raw):
|
|
||||||
ctx_kwargs = {
|
|
||||||
key.replace("ctx_", ""): raw.pop(key)
|
|
||||||
for key in {**raw}.keys()
|
|
||||||
if key.startswith("ctx_")
|
|
||||||
}
|
|
||||||
restricted = [
|
|
||||||
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
|
|
||||||
]
|
|
||||||
if restricted:
|
|
||||||
restricted_arguments = ", ".join(restricted)
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use restricted route context: "
|
|
||||||
f"{restricted_arguments}. This limitation is only in place "
|
|
||||||
"until v22.3 when the restricted names will no longer be in"
|
|
||||||
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
|
|
||||||
"for more information."
|
|
||||||
)
|
|
||||||
if raw:
|
|
||||||
unexpected_arguments = ", ".join(raw.keys())
|
|
||||||
raise TypeError(
|
|
||||||
f"Unexpected keyword arguments: {unexpected_arguments}"
|
|
||||||
)
|
|
||||||
return HashableDict(ctx_kwargs)
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
from enum import Enum
|
from typing import Any, Callable, Dict, Optional, Set
|
||||||
from typing import Any, Callable, Dict, Optional, Set, Union
|
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
|
||||||
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
|
||||||
from sanic.signals import Signal
|
from sanic.signals import Signal
|
||||||
from sanic.types import HashableDict
|
|
||||||
|
|
||||||
|
|
||||||
class SignalMixin(metaclass=SanicMeta):
|
class HashableDict(dict):
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(tuple(sorted(self.items())))
|
||||||
|
|
||||||
|
|
||||||
|
class SignalMixin:
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_signals: Set[FutureSignal] = set()
|
self._future_signals: Set[FutureSignal] = set()
|
||||||
|
|
||||||
@@ -17,11 +19,10 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
def signal(
|
def signal(
|
||||||
self,
|
self,
|
||||||
event: Union[str, Enum],
|
event: str,
|
||||||
*,
|
*,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
exclusive: bool = True,
|
|
||||||
) -> Callable[[SignalHandler], SignalHandler]:
|
) -> Callable[[SignalHandler], SignalHandler]:
|
||||||
"""
|
"""
|
||||||
For creating a signal handler, used similar to a route handler:
|
For creating a signal handler, used similar to a route handler:
|
||||||
@@ -34,22 +35,19 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
:param event: Representation of the event in ``one.two.three`` form
|
:param event: Representation of the event in ``one.two.three`` form
|
||||||
:type event: str
|
:type event: str
|
||||||
:param apply: For lazy evaluation, defaults to ``True``
|
:param apply: For lazy evaluation, defaults to True
|
||||||
:type apply: bool, optional
|
:type apply: bool, optional
|
||||||
:param condition: For use with the ``condition`` argument in dispatch
|
:param condition: For use with the ``condition`` argument in dispatch
|
||||||
filtering, defaults to ``None``
|
filtering, defaults to None
|
||||||
:param exclusive: When ``True``, the signal can only be dispatched
|
|
||||||
when the condition has been met. When ``False``, the signal can
|
|
||||||
be dispatched either with or without it. *THIS IS INAPPLICABLE TO
|
|
||||||
BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults
|
|
||||||
to ``True``
|
|
||||||
:type condition: Dict[str, Any], optional
|
:type condition: Dict[str, Any], optional
|
||||||
"""
|
"""
|
||||||
event_value = str(event.value) if isinstance(event, Enum) else event
|
|
||||||
|
|
||||||
def decorator(handler: SignalHandler):
|
def decorator(handler: SignalHandler):
|
||||||
|
nonlocal event
|
||||||
|
nonlocal apply
|
||||||
|
|
||||||
future_signal = FutureSignal(
|
future_signal = FutureSignal(
|
||||||
handler, event_value, HashableDict(condition or {}), exclusive
|
handler, event, HashableDict(condition or {})
|
||||||
)
|
)
|
||||||
self._future_signals.add(future_signal)
|
self._future_signals.add(future_signal)
|
||||||
|
|
||||||
@@ -65,7 +63,6 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
handler: Optional[Callable[..., Any]],
|
handler: Optional[Callable[..., Any]],
|
||||||
event: str,
|
event: str,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
exclusive: bool = True,
|
|
||||||
):
|
):
|
||||||
if not handler:
|
if not handler:
|
||||||
|
|
||||||
@@ -73,9 +70,7 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
...
|
...
|
||||||
|
|
||||||
handler = noop
|
handler = noop
|
||||||
self.signal(event=event, condition=condition, exclusive=exclusive)(
|
self.signal(event=event, condition=condition)(handler)
|
||||||
handler
|
|
||||||
)
|
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def event(self, event: str):
|
def event(self, event: str):
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
||||||
|
|
||||||
@@ -15,20 +14,10 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
|
|||||||
|
|
||||||
class MockProtocol:
|
class MockProtocol:
|
||||||
def __init__(self, transport: "MockTransport", loop):
|
def __init__(self, transport: "MockTransport", loop):
|
||||||
# This should be refactored when < 3.8 support is dropped
|
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
# Fixup for 3.8+; Sanic still supports 3.7 where loop is required
|
self._not_paused = asyncio.Event(loop=loop)
|
||||||
loop = loop if sys.version_info[:2] < (3, 8) else None
|
self._not_paused.set()
|
||||||
# Optional in 3.9, necessary in 3.10 because the parameter "loop"
|
self._complete = asyncio.Event(loop=loop)
|
||||||
# was completely removed
|
|
||||||
if not loop:
|
|
||||||
self._not_paused = asyncio.Event()
|
|
||||||
self._not_paused.set()
|
|
||||||
self._complete = asyncio.Event()
|
|
||||||
else:
|
|
||||||
self._not_paused = asyncio.Event(loop=loop)
|
|
||||||
self._not_paused.set()
|
|
||||||
self._complete = asyncio.Event(loop=loop)
|
|
||||||
|
|
||||||
def pause_writing(self) -> None:
|
def pause_writing(self) -> None:
|
||||||
self._not_paused.clear()
|
self._not_paused.clear()
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ from sanic.models.handler_types import (
|
|||||||
MiddlewareType,
|
MiddlewareType,
|
||||||
SignalHandler,
|
SignalHandler,
|
||||||
)
|
)
|
||||||
from sanic.types import HashableDict
|
|
||||||
|
|
||||||
|
|
||||||
class FutureRoute(NamedTuple):
|
class FutureRoute(NamedTuple):
|
||||||
handler: str
|
handler: str
|
||||||
uri: str
|
uri: str
|
||||||
methods: Optional[Iterable[str]]
|
methods: Optional[Iterable[str]]
|
||||||
host: Union[str, List[str]]
|
host: str
|
||||||
strict_slashes: bool
|
strict_slashes: bool
|
||||||
stream: bool
|
stream: bool
|
||||||
version: Optional[int]
|
version: Optional[int]
|
||||||
@@ -26,7 +25,6 @@ class FutureRoute(NamedTuple):
|
|||||||
static: bool
|
static: bool
|
||||||
version_prefix: str
|
version_prefix: str
|
||||||
error_format: Optional[str]
|
error_format: Optional[str]
|
||||||
route_context: HashableDict
|
|
||||||
|
|
||||||
|
|
||||||
class FutureListener(NamedTuple):
|
class FutureListener(NamedTuple):
|
||||||
@@ -62,8 +60,3 @@ class FutureSignal(NamedTuple):
|
|||||||
handler: SignalHandler
|
handler: SignalHandler
|
||||||
event: str
|
event: str
|
||||||
condition: Optional[Dict[str, str]]
|
condition: Optional[Dict[str, str]]
|
||||||
exclusive: bool
|
|
||||||
|
|
||||||
|
|
||||||
class FutureRegistry(set):
|
|
||||||
...
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from ssl import SSLObject
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
from sanic.models.protocol_types import TransportProtocol
|
from sanic.models.protocol_types import TransportProtocol
|
||||||
|
|
||||||
@@ -22,10 +20,8 @@ class ConnInfo:
|
|||||||
"peername",
|
"peername",
|
||||||
"server_port",
|
"server_port",
|
||||||
"server",
|
"server",
|
||||||
"server_name",
|
|
||||||
"sockname",
|
"sockname",
|
||||||
"ssl",
|
"ssl",
|
||||||
"cert",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, transport: TransportProtocol, unix=None):
|
def __init__(self, transport: TransportProtocol, unix=None):
|
||||||
@@ -35,16 +31,8 @@ class ConnInfo:
|
|||||||
self.server_port = self.client_port = 0
|
self.server_port = self.client_port = 0
|
||||||
self.client_ip = ""
|
self.client_ip = ""
|
||||||
self.sockname = addr = transport.get_extra_info("sockname")
|
self.sockname = addr = transport.get_extra_info("sockname")
|
||||||
self.ssl = False
|
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
|
||||||
self.server_name = ""
|
|
||||||
self.cert: Dict[str, Any] = {}
|
|
||||||
sslobj: Optional[SSLObject] = transport.get_extra_info(
|
|
||||||
"ssl_object"
|
|
||||||
) # type: ignore
|
|
||||||
if sslobj:
|
|
||||||
self.ssl = True
|
|
||||||
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
|
|
||||||
self.cert = dict(getattr(sslobj.context, "sanic", {}))
|
|
||||||
if isinstance(addr, str): # UNIX socket
|
if isinstance(addr, str): # UNIX socket
|
||||||
self.server = unix or addr
|
self.server = unix or addr
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ 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.
|
||||||
@@ -47,19 +50,13 @@ def _get_args_for_reloading():
|
|||||||
return [sys.executable] + sys.argv
|
return [sys.executable] + sys.argv
|
||||||
|
|
||||||
|
|
||||||
def restart_with_reloader(changed=None):
|
def restart_with_reloader():
|
||||||
"""Create a new process and a subprocess in it with the same arguments as
|
"""Create a new process and a subprocess in it with the same arguments as
|
||||||
this one.
|
this one.
|
||||||
"""
|
"""
|
||||||
reloaded = ",".join(changed) if changed else ""
|
|
||||||
return subprocess.Popen(
|
return subprocess.Popen(
|
||||||
_get_args_for_reloading(),
|
_get_args_for_reloading(),
|
||||||
env={
|
env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
|
||||||
**os.environ,
|
|
||||||
"SANIC_SERVER_RUNNING": "true",
|
|
||||||
"SANIC_RELOADER_PROCESS": "true",
|
|
||||||
"SANIC_RELOADED_FILES": reloaded,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -94,29 +91,31 @@ 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
|
||||||
|
|
||||||
changed = set()
|
|
||||||
for filename in itertools.chain(
|
for filename in itertools.chain(
|
||||||
_iter_module_files(),
|
_iter_module_files(),
|
||||||
*(d.glob("**/*") for d in app.reload_dirs),
|
*(d.glob("**/*") for d in app.reload_dirs),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
if _check_file(filename, mtimes):
|
check = _check_file(filename, mtimes)
|
||||||
path = (
|
|
||||||
filename
|
|
||||||
if isinstance(filename, str)
|
|
||||||
else filename.resolve()
|
|
||||||
)
|
|
||||||
changed.add(str(path))
|
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if changed:
|
if check:
|
||||||
|
need_reload = True
|
||||||
|
|
||||||
|
if need_reload:
|
||||||
worker_process.terminate()
|
worker_process.terminate()
|
||||||
worker_process.wait()
|
worker_process.wait()
|
||||||
worker_process = restart_with_reloader(changed)
|
worker_process = restart_with_reloader()
|
||||||
|
|
||||||
sleep(sleep_interval)
|
sleep(sleep_interval)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ from typing import (
|
|||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING:
|
||||||
from sanic.server import ConnInfo
|
from sanic.server import ConnInfo
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
from sanic.http import Http
|
||||||
|
|
||||||
import email.utils
|
import email.utils
|
||||||
import uuid
|
import uuid
|
||||||
@@ -31,7 +32,7 @@ from httptools import parse_url # type: ignore
|
|||||||
|
|
||||||
from sanic.compat import CancelledErrors, Header
|
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, ServerError
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.headers import (
|
from sanic.headers import (
|
||||||
AcceptContainer,
|
AcceptContainer,
|
||||||
Options,
|
Options,
|
||||||
@@ -41,7 +42,6 @@ from sanic.headers import (
|
|||||||
parse_host,
|
parse_host,
|
||||||
parse_xforwarded,
|
parse_xforwarded,
|
||||||
)
|
)
|
||||||
from sanic.http import Http, Stage
|
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, logger
|
||||||
from sanic.models.protocol_types import TransportProtocol
|
from sanic.models.protocol_types import TransportProtocol
|
||||||
from sanic.response import BaseHTTPResponse, HTTPResponse
|
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||||
@@ -104,7 +104,6 @@ class Request:
|
|||||||
"parsed_json",
|
"parsed_json",
|
||||||
"parsed_forwarded",
|
"parsed_forwarded",
|
||||||
"raw_url",
|
"raw_url",
|
||||||
"responded",
|
|
||||||
"request_middleware_started",
|
"request_middleware_started",
|
||||||
"route",
|
"route",
|
||||||
"stream",
|
"stream",
|
||||||
@@ -156,7 +155,6 @@ class Request:
|
|||||||
self.stream: Optional[Http] = None
|
self.stream: Optional[Http] = None
|
||||||
self.route: Optional[Route] = None
|
self.route: Optional[Route] = None
|
||||||
self._protocol = None
|
self._protocol = None
|
||||||
self.responded: bool = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
@@ -166,21 +164,6 @@ class Request:
|
|||||||
def generate_id(*_):
|
def generate_id(*_):
|
||||||
return uuid.uuid4()
|
return uuid.uuid4()
|
||||||
|
|
||||||
def reset_response(self):
|
|
||||||
try:
|
|
||||||
if (
|
|
||||||
self.stream is not None
|
|
||||||
and self.stream.stage is not Stage.HANDLER
|
|
||||||
):
|
|
||||||
raise ServerError(
|
|
||||||
"Cannot reset response because previous response was sent."
|
|
||||||
)
|
|
||||||
self.stream.response.stream = None
|
|
||||||
self.stream.response = None
|
|
||||||
self.responded = False
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def respond(
|
async def respond(
|
||||||
self,
|
self,
|
||||||
response: Optional[BaseHTTPResponse] = None,
|
response: Optional[BaseHTTPResponse] = None,
|
||||||
@@ -189,19 +172,13 @@ class Request:
|
|||||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
content_type: Optional[str] = None,
|
content_type: Optional[str] = None,
|
||||||
):
|
):
|
||||||
try:
|
|
||||||
if self.stream is not None and self.stream.response:
|
|
||||||
raise ServerError("Second respond call is not allowed.")
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
# This logic of determining which response to use is subject to change
|
# This logic of determining which response to use is subject to change
|
||||||
if response is None:
|
if response is None:
|
||||||
response = HTTPResponse(
|
response = (self.stream and self.stream.response) or HTTPResponse(
|
||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Connect the response
|
# Connect the response
|
||||||
if isinstance(response, BaseHTTPResponse) and self.stream:
|
if isinstance(response, BaseHTTPResponse) and self.stream:
|
||||||
response = self.stream.respond(response)
|
response = self.stream.respond(response)
|
||||||
@@ -216,7 +193,6 @@ class Request:
|
|||||||
error_logger.exception(
|
error_logger.exception(
|
||||||
"Exception occurred in one of response middleware handlers"
|
"Exception occurred in one of response middleware handlers"
|
||||||
)
|
)
|
||||||
self.responded = True
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def receive_body(self):
|
async def receive_body(self):
|
||||||
@@ -784,10 +760,9 @@ def parse_multipart_form(body, boundary):
|
|||||||
break
|
break
|
||||||
|
|
||||||
colon_index = form_line.index(":")
|
colon_index = form_line.index(":")
|
||||||
idx = colon_index + 2
|
|
||||||
form_header_field = form_line[0:colon_index].lower()
|
form_header_field = form_line[0:colon_index].lower()
|
||||||
form_header_value, form_parameters = parse_content_header(
|
form_header_value, form_parameters = parse_content_header(
|
||||||
form_line[idx:]
|
form_line[colon_index + 2 :]
|
||||||
)
|
)
|
||||||
|
|
||||||
if form_header_field == "content-disposition":
|
if form_header_field == "content-disposition":
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
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 typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
Any,
|
||||||
AnyStr,
|
AnyStr,
|
||||||
Callable,
|
Callable,
|
||||||
@@ -14,27 +11,19 @@ from typing import (
|
|||||||
Iterator,
|
Iterator,
|
||||||
Optional,
|
Optional,
|
||||||
Tuple,
|
Tuple,
|
||||||
TypeVar,
|
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.compat import Header, open_async
|
from sanic.compat import Header, open_async
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||||
from sanic.cookies import CookieJar
|
from sanic.cookies import CookieJar
|
||||||
from sanic.exceptions import SanicException, ServerError
|
|
||||||
from sanic.helpers import has_message_body, remove_entity_headers
|
from sanic.helpers import has_message_body, remove_entity_headers
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
from sanic.models.protocol_types import HTMLProtocol, Range
|
from sanic.models.protocol_types import HTMLProtocol, Range
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic.asgi import ASGIApp
|
|
||||||
from sanic.request import Request
|
|
||||||
else:
|
|
||||||
Request = TypeVar("Request")
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ujson import dumps as json_dumps
|
from ujson import dumps as json_dumps
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -56,7 +45,7 @@ class BaseHTTPResponse:
|
|||||||
self.asgi: bool = False
|
self.asgi: bool = False
|
||||||
self.body: Optional[bytes] = None
|
self.body: Optional[bytes] = None
|
||||||
self.content_type: Optional[str] = None
|
self.content_type: Optional[str] = None
|
||||||
self.stream: Optional[Union[Http, ASGIApp]] = None
|
self.stream: Http = None
|
||||||
self.status: int = None
|
self.status: int = None
|
||||||
self.headers = Header({})
|
self.headers = Header({})
|
||||||
self._cookies: Optional[CookieJar] = None
|
self._cookies: Optional[CookieJar] = None
|
||||||
@@ -112,7 +101,7 @@ class BaseHTTPResponse:
|
|||||||
|
|
||||||
async def send(
|
async def send(
|
||||||
self,
|
self,
|
||||||
data: Optional[AnyStr] = None,
|
data: Optional[Union[AnyStr]] = None,
|
||||||
end_stream: Optional[bool] = None,
|
end_stream: Optional[bool] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -123,17 +112,8 @@ class BaseHTTPResponse:
|
|||||||
"""
|
"""
|
||||||
if data is None and end_stream is None:
|
if data is None and end_stream is None:
|
||||||
end_stream = True
|
end_stream = True
|
||||||
if self.stream is None:
|
if end_stream and not data and self.stream.send is None:
|
||||||
raise SanicException(
|
return
|
||||||
"No stream is connected to the response object instance."
|
|
||||||
)
|
|
||||||
if self.stream.send is None:
|
|
||||||
if end_stream and not data:
|
|
||||||
return
|
|
||||||
raise ServerError(
|
|
||||||
"Response stream was ended, no more response data is "
|
|
||||||
"allowed to be sent."
|
|
||||||
)
|
|
||||||
data = (
|
data = (
|
||||||
data.encode() # type: ignore
|
data.encode() # type: ignore
|
||||||
if hasattr(data, "encode")
|
if hasattr(data, "encode")
|
||||||
@@ -142,6 +122,95 @@ class BaseHTTPResponse:
|
|||||||
await self.stream.send(data, end_stream=end_stream)
|
await self.stream.send(data, end_stream=end_stream)
|
||||||
|
|
||||||
|
|
||||||
|
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
|
||||||
|
|
||||||
|
|
||||||
|
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
|
"""
|
||||||
|
Old style streaming response where you pass a streaming function:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def sample_streaming_fn(response):
|
||||||
|
await response.write("foo")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await response.write("bar")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def test(request):
|
||||||
|
return stream(sample_streaming_fn)
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
**Deprecated** and set for removal in v21.12. You can now achieve the
|
||||||
|
same functionality without a callback.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def test(request):
|
||||||
|
response = await request.respond()
|
||||||
|
await response.send("foo", False)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await response.send("bar", False)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await response.send("", True)
|
||||||
|
return response
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"streaming_fn",
|
||||||
|
"status",
|
||||||
|
"content_type",
|
||||||
|
"headers",
|
||||||
|
"_cookies",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
streaming_fn: StreamingFunction,
|
||||||
|
status: int = 200,
|
||||||
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
|
content_type: str = "text/plain; charset=utf-8",
|
||||||
|
ignore_deprecation_notice: bool = False,
|
||||||
|
):
|
||||||
|
if not ignore_deprecation_notice:
|
||||||
|
warn(
|
||||||
|
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
|
||||||
|
"will be removed in v21.12. Please upgrade your streaming "
|
||||||
|
"response implementation. You can learn more here: "
|
||||||
|
"https://sanicframework.org/en/guide/advanced/streaming.html"
|
||||||
|
"#response-streaming. If you use the builtin stream() or "
|
||||||
|
"file_stream() methods, this upgrade will be be done for you."
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.content_type = content_type
|
||||||
|
self.streaming_fn = streaming_fn
|
||||||
|
self.status = status
|
||||||
|
self.headers = Header(headers or {})
|
||||||
|
self._cookies = None
|
||||||
|
|
||||||
|
async def write(self, data):
|
||||||
|
"""Writes a chunk of data to the streaming response.
|
||||||
|
|
||||||
|
:param data: str or bytes-ish data to be written.
|
||||||
|
"""
|
||||||
|
await super().send(self._encode_body(data))
|
||||||
|
|
||||||
|
async def send(self, *args, **kwargs):
|
||||||
|
if self.streaming_fn is not None:
|
||||||
|
await self.streaming_fn(self)
|
||||||
|
self.streaming_fn = None
|
||||||
|
await super().send(*args, **kwargs)
|
||||||
|
|
||||||
|
async def eof(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class HTTPResponse(BaseHTTPResponse):
|
class HTTPResponse(BaseHTTPResponse):
|
||||||
"""
|
"""
|
||||||
HTTP response to be sent back to the client.
|
HTTP response to be sent back to the client.
|
||||||
@@ -336,109 +405,6 @@ async def file(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def redirect(
|
|
||||||
to: str,
|
|
||||||
headers: Optional[Dict[str, str]] = None,
|
|
||||||
status: int = 302,
|
|
||||||
content_type: str = "text/html; charset=utf-8",
|
|
||||||
) -> HTTPResponse:
|
|
||||||
"""
|
|
||||||
Abort execution and cause a 302 redirect (by default) by setting a
|
|
||||||
Location header.
|
|
||||||
|
|
||||||
:param to: path or fully qualified URL to redirect to
|
|
||||||
:param headers: optional dict of headers to include in the new request
|
|
||||||
:param status: status code (int) of the new request, defaults to 302
|
|
||||||
:param content_type: the content type (string) of the response
|
|
||||||
"""
|
|
||||||
headers = headers or {}
|
|
||||||
|
|
||||||
# URL Quote the URL before redirecting
|
|
||||||
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
|
||||||
|
|
||||||
# According to RFC 7231, a relative URI is now permitted.
|
|
||||||
headers["Location"] = safe_to
|
|
||||||
|
|
||||||
return HTTPResponse(
|
|
||||||
status=status, headers=headers, content_type=content_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseStream:
|
|
||||||
"""
|
|
||||||
ResponseStream is a compat layer to bridge the gap after the deprecation
|
|
||||||
of StreamingHTTPResponse. In v22.6 it will be removed when:
|
|
||||||
- stream is removed
|
|
||||||
- file_stream is moved to new style streaming
|
|
||||||
- file and file_stream are combined into a single API
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"_cookies",
|
|
||||||
"content_type",
|
|
||||||
"headers",
|
|
||||||
"request",
|
|
||||||
"response",
|
|
||||||
"status",
|
|
||||||
"streaming_fn",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
streaming_fn: Callable[
|
|
||||||
[Union[BaseHTTPResponse, ResponseStream]],
|
|
||||||
Coroutine[Any, Any, None],
|
|
||||||
],
|
|
||||||
status: int = 200,
|
|
||||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
|
||||||
content_type: Optional[str] = None,
|
|
||||||
):
|
|
||||||
self.streaming_fn = streaming_fn
|
|
||||||
self.status = status
|
|
||||||
self.headers = headers or Header()
|
|
||||||
self.content_type = content_type
|
|
||||||
self.request: Optional[Request] = None
|
|
||||||
self._cookies: Optional[CookieJar] = None
|
|
||||||
|
|
||||||
async def write(self, message: str):
|
|
||||||
await self.response.send(message)
|
|
||||||
|
|
||||||
async def stream(self) -> HTTPResponse:
|
|
||||||
if not self.request:
|
|
||||||
raise ServerError("Attempted response to unknown request")
|
|
||||||
self.response = await self.request.respond(
|
|
||||||
headers=self.headers,
|
|
||||||
status=self.status,
|
|
||||||
content_type=self.content_type,
|
|
||||||
)
|
|
||||||
await self.streaming_fn(self)
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
async def eof(self) -> None:
|
|
||||||
await self.response.eof()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cookies(self) -> CookieJar:
|
|
||||||
if self._cookies is None:
|
|
||||||
self._cookies = CookieJar(self.headers)
|
|
||||||
return self._cookies
|
|
||||||
|
|
||||||
@property
|
|
||||||
def processed_headers(self):
|
|
||||||
return self.response.processed_headers
|
|
||||||
|
|
||||||
@property
|
|
||||||
def body(self):
|
|
||||||
return self.response.body
|
|
||||||
|
|
||||||
def __call__(self, request: Request) -> ResponseStream:
|
|
||||||
self.request = request
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __await__(self):
|
|
||||||
return self.stream().__await__()
|
|
||||||
|
|
||||||
|
|
||||||
async def file_stream(
|
async def file_stream(
|
||||||
location: Union[str, PurePath],
|
location: Union[str, PurePath],
|
||||||
status: int = 200,
|
status: int = 200,
|
||||||
@@ -447,7 +413,7 @@ async def file_stream(
|
|||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
filename: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
_range: Optional[Range] = None,
|
_range: Optional[Range] = None,
|
||||||
) -> ResponseStream:
|
) -> StreamingHTTPResponse:
|
||||||
"""Return a streaming response object with file data.
|
"""Return a streaming response object with file data.
|
||||||
|
|
||||||
:param location: Location of file on system.
|
:param location: Location of file on system.
|
||||||
@@ -455,6 +421,7 @@ async def file_stream(
|
|||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param filename: Override filename.
|
:param filename: Override filename.
|
||||||
|
:param chunked: Deprecated
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
@@ -490,24 +457,23 @@ async def file_stream(
|
|||||||
break
|
break
|
||||||
await response.write(content)
|
await response.write(content)
|
||||||
|
|
||||||
return ResponseStream(
|
return StreamingHTTPResponse(
|
||||||
streaming_fn=_streaming_fn,
|
streaming_fn=_streaming_fn,
|
||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
|
ignore_deprecation_notice=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
def stream(
|
||||||
streaming_fn: Callable[
|
streaming_fn: StreamingFunction,
|
||||||
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
|
|
||||||
],
|
|
||||||
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",
|
||||||
) -> ResponseStream:
|
):
|
||||||
"""Accepts a 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 `ResponseStream`.
|
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||||
|
|
||||||
Example usage::
|
Example usage::
|
||||||
|
|
||||||
@@ -521,13 +487,42 @@ def stream(
|
|||||||
|
|
||||||
:param streaming_fn: A coroutine accepts a response and
|
:param streaming_fn: A coroutine accepts a response and
|
||||||
writes content to that response.
|
writes content to that response.
|
||||||
:param status: HTTP status.
|
:param mime_type: Specific mime_type.
|
||||||
:param content_type: Specific content_type.
|
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
:param chunked: Deprecated
|
||||||
"""
|
"""
|
||||||
return ResponseStream(
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def redirect(
|
||||||
|
to: str,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
status: int = 302,
|
||||||
|
content_type: str = "text/html; charset=utf-8",
|
||||||
|
) -> HTTPResponse:
|
||||||
|
"""
|
||||||
|
Abort execution and cause a 302 redirect (by default) by setting a
|
||||||
|
Location header.
|
||||||
|
|
||||||
|
:param to: path or fully qualified URL to redirect to
|
||||||
|
:param headers: optional dict of headers to include in the new request
|
||||||
|
:param status: status code (int) of the new request, defaults to 302
|
||||||
|
:param content_type: the content type (string) of the response
|
||||||
|
"""
|
||||||
|
headers = headers or {}
|
||||||
|
|
||||||
|
# URL Quote the URL before redirecting
|
||||||
|
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
||||||
|
|
||||||
|
# According to RFC 7231, a relative URI is now permitted.
|
||||||
|
headers["Location"] = safe_to
|
||||||
|
|
||||||
|
return HTTPResponse(
|
||||||
|
status=status, headers=headers, content_type=content_type
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class Router(BaseRouter):
|
|||||||
self, path: str, method: str, host: Optional[str]
|
self, path: str, method: str, host: Optional[str]
|
||||||
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Retrieve a `Route` object containing the details about how to handle
|
Retrieve a `Route` object containg the details about how to handle
|
||||||
a response for a given request
|
a response for a given request
|
||||||
|
|
||||||
:param request: the incoming request object
|
:param request: the incoming request object
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
from sanic.models.server_types import ConnInfo, Signal
|
from sanic.models.server_types import ConnInfo, Signal
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
from sanic.server.loop import try_use_uvloop
|
|
||||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||||
from sanic.server.runners import serve, serve_multiple, serve_single
|
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__ = (
|
__all__ = (
|
||||||
"AsyncioServer",
|
"AsyncioServer",
|
||||||
"ConnInfo",
|
"ConnInfo",
|
||||||
@@ -13,5 +23,4 @@ __all__ = (
|
|||||||
"serve",
|
"serve",
|
||||||
"serve_multiple",
|
"serve_multiple",
|
||||||
"serve_single",
|
"serve_single",
|
||||||
"try_use_uvloop",
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.log import deprecation
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncioServer:
|
class AsyncioServer:
|
||||||
@@ -18,11 +11,11 @@ class AsyncioServer:
|
|||||||
a user who needs to manage the server lifecycle manually.
|
a user who needs to manage the server lifecycle manually.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("app", "connections", "loop", "serve_coro", "server")
|
__slots__ = ("app", "connections", "loop", "serve_coro", "server", "init")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
app: Sanic,
|
app,
|
||||||
loop,
|
loop,
|
||||||
serve_coro,
|
serve_coro,
|
||||||
connections,
|
connections,
|
||||||
@@ -34,20 +27,13 @@ class AsyncioServer:
|
|||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.serve_coro = serve_coro
|
self.serve_coro = serve_coro
|
||||||
self.server = None
|
self.server = None
|
||||||
|
self.init = False
|
||||||
@property
|
|
||||||
def init(self):
|
|
||||||
deprecation(
|
|
||||||
"AsyncioServer.init has been deprecated and will be removed "
|
|
||||||
"in v22.6. Use Sanic.state.is_started instead.",
|
|
||||||
22.6,
|
|
||||||
)
|
|
||||||
return self.app.state.is_started
|
|
||||||
|
|
||||||
def startup(self):
|
def startup(self):
|
||||||
"""
|
"""
|
||||||
Trigger "before_server_start" events
|
Trigger "before_server_start" events
|
||||||
"""
|
"""
|
||||||
|
self.init = True
|
||||||
return self.app._startup()
|
return self.app._startup()
|
||||||
|
|
||||||
def before_start(self):
|
def before_start(self):
|
||||||
@@ -91,33 +77,30 @@ class AsyncioServer:
|
|||||||
return task
|
return task
|
||||||
|
|
||||||
def start_serving(self):
|
def start_serving(self):
|
||||||
return self._serve(self.server.start_serving)
|
|
||||||
|
|
||||||
def serve_forever(self):
|
|
||||||
return self._serve(self.server.serve_forever)
|
|
||||||
|
|
||||||
def _serve(self, serve_func):
|
|
||||||
if self.server:
|
if self.server:
|
||||||
if not self.app.state.is_started:
|
try:
|
||||||
raise SanicException(
|
return self.server.start_serving()
|
||||||
"Cannot run Sanic server without first running "
|
except AttributeError:
|
||||||
"await server.startup()"
|
raise NotImplementedError(
|
||||||
|
"server.start_serving not available in this version "
|
||||||
|
"of asyncio or uvloop."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def serve_forever(self):
|
||||||
|
if self.server:
|
||||||
try:
|
try:
|
||||||
return serve_func()
|
return self.server.serve_forever()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
name = serve_func.__name__
|
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"server.{name} not available in this version "
|
"server.serve_forever not available in this version "
|
||||||
"of asyncio or uvloop."
|
"of asyncio or uvloop."
|
||||||
)
|
)
|
||||||
|
|
||||||
def _server_event(self, concern: str, action: str):
|
def _server_event(self, concern: str, action: str):
|
||||||
if not self.app.state.is_started:
|
if not self.init:
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
"Cannot dispatch server event without "
|
"Cannot dispatch server event without "
|
||||||
"first running await server.startup()"
|
"first running server.startup()"
|
||||||
)
|
)
|
||||||
return self.app._server_event(concern, action, loop=self.loop)
|
return self.app._server_event(concern, action, loop=self.loop)
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
from distutils.util import strtobool
|
|
||||||
from os import getenv
|
|
||||||
|
|
||||||
from sanic.compat import OS_IS_WINDOWS
|
|
||||||
from sanic.log import error_logger
|
|
||||||
|
|
||||||
|
|
||||||
def try_use_uvloop() -> None:
|
|
||||||
"""
|
|
||||||
Use uvloop instead of the default asyncio loop.
|
|
||||||
"""
|
|
||||||
if OS_IS_WINDOWS:
|
|
||||||
error_logger.warning(
|
|
||||||
"You are trying to use uvloop, but uvloop is not compatible "
|
|
||||||
"with your system. You can disable uvloop completely by setting "
|
|
||||||
"the 'USE_UVLOOP' configuration value to false, or simply not "
|
|
||||||
"defining it and letting Sanic handle it for you. Sanic will now "
|
|
||||||
"continue to run using the default event loop."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
import uvloop # type: ignore
|
|
||||||
except ImportError:
|
|
||||||
error_logger.warning(
|
|
||||||
"You are trying to use uvloop, but uvloop is not "
|
|
||||||
"installed in your system. In order to use uvloop "
|
|
||||||
"you must first install it. Otherwise, you can disable "
|
|
||||||
"uvloop completely by setting the 'USE_UVLOOP' "
|
|
||||||
"configuration value to false. Sanic will now continue "
|
|
||||||
"to run with the default event loop."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no"))
|
|
||||||
if uvloop_install_removed:
|
|
||||||
error_logger.info(
|
|
||||||
"You are requesting to run Sanic using uvloop, but the "
|
|
||||||
"install-time 'SANIC_NO_UVLOOP' environment variable (used to "
|
|
||||||
"opt-out of installing uvloop with Sanic) is set to true. If "
|
|
||||||
"you want to prevent Sanic from overriding the event loop policy "
|
|
||||||
"during runtime, set the 'USE_UVLOOP' configuration value to "
|
|
||||||
"false."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
from typing import TYPE_CHECKING, Optional, Sequence, cast
|
from typing import TYPE_CHECKING, Optional, Sequence
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN
|
from websockets.connection import CLOSED, CLOSING, OPEN
|
||||||
from websockets.server import ServerConnection
|
from websockets.server import ServerConnection
|
||||||
from websockets.typing import Subprotocol
|
|
||||||
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.server import HttpProtocol
|
from sanic.server import HttpProtocol
|
||||||
|
|
||||||
from ..websockets.impl import WebsocketImplProtocol
|
from ..websockets.impl import WebsocketImplProtocol
|
||||||
@@ -16,13 +15,12 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class WebSocketProtocol(HttpProtocol):
|
class WebSocketProtocol(HttpProtocol):
|
||||||
__slots__ = (
|
|
||||||
"websocket",
|
websocket: Optional[WebsocketImplProtocol]
|
||||||
"websocket_timeout",
|
websocket_timeout: float
|
||||||
"websocket_max_size",
|
websocket_max_size = Optional[int]
|
||||||
"websocket_ping_interval",
|
websocket_ping_interval = Optional[float]
|
||||||
"websocket_ping_timeout",
|
websocket_ping_timeout = Optional[float]
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -37,29 +35,32 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.websocket: Optional[WebsocketImplProtocol] = None
|
self.websocket = None
|
||||||
self.websocket_timeout = websocket_timeout
|
self.websocket_timeout = websocket_timeout
|
||||||
self.websocket_max_size = websocket_max_size
|
self.websocket_max_size = websocket_max_size
|
||||||
if websocket_max_queue is not None and websocket_max_queue > 0:
|
if websocket_max_queue is not None and websocket_max_queue > 0:
|
||||||
# TODO: Reminder remove this warning in v22.3
|
# TODO: Reminder remove this warning in v22.3
|
||||||
deprecation(
|
error_logger.warning(
|
||||||
"Websocket no longer uses queueing, so websocket_max_queue"
|
DeprecationWarning(
|
||||||
" is no longer required.",
|
"Websocket no longer uses queueing, so websocket_max_queue"
|
||||||
22.3,
|
" is no longer required."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if websocket_read_limit is not None and websocket_read_limit > 0:
|
if websocket_read_limit is not None and websocket_read_limit > 0:
|
||||||
# TODO: Reminder remove this warning in v22.3
|
# TODO: Reminder remove this warning in v22.3
|
||||||
deprecation(
|
error_logger.warning(
|
||||||
"Websocket no longer uses read buffers, so "
|
DeprecationWarning(
|
||||||
"websocket_read_limit is not required.",
|
"Websocket no longer uses read buffers, so "
|
||||||
22.3,
|
"websocket_read_limit is not required."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if websocket_write_limit is not None and websocket_write_limit > 0:
|
if websocket_write_limit is not None and websocket_write_limit > 0:
|
||||||
# TODO: Reminder remove this warning in v22.3
|
# TODO: Reminder remove this warning in v22.3
|
||||||
deprecation(
|
error_logger.warning(
|
||||||
"Websocket no longer uses write buffers, so "
|
DeprecationWarning(
|
||||||
"websocket_write_limit is not required.",
|
"Websocket no longer uses write buffers, so "
|
||||||
22.3,
|
"websocket_write_limit is not required."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.websocket_ping_interval = websocket_ping_interval
|
self.websocket_ping_interval = websocket_ping_interval
|
||||||
self.websocket_ping_timeout = websocket_ping_timeout
|
self.websocket_ping_timeout = websocket_ping_timeout
|
||||||
@@ -108,22 +109,14 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
return super().close_if_idle()
|
return super().close_if_idle()
|
||||||
|
|
||||||
async def websocket_handshake(
|
async def websocket_handshake(
|
||||||
self, request, subprotocols: Optional[Sequence[str]] = None
|
self, request, subprotocols=Optional[Sequence[str]]
|
||||||
):
|
):
|
||||||
# let the websockets package do the handshake with the client
|
# let the websockets package do the handshake with the client
|
||||||
try:
|
try:
|
||||||
if subprotocols is not None:
|
if subprotocols is not None:
|
||||||
# subprotocols can be a set or frozenset,
|
# subprotocols can be a set or frozenset,
|
||||||
# but ServerConnection needs a list
|
# but ServerConnection needs a list
|
||||||
subprotocols = cast(
|
subprotocols = list(subprotocols)
|
||||||
Optional[Sequence[Subprotocol]],
|
|
||||||
list(
|
|
||||||
[
|
|
||||||
Subprotocol(subprotocol)
|
|
||||||
for subprotocol in subprotocols
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
ws_conn = ServerConnection(
|
ws_conn = ServerConnection(
|
||||||
max_size=self.websocket_max_size,
|
max_size=self.websocket_max_size,
|
||||||
subprotocols=subprotocols,
|
subprotocols=subprotocols,
|
||||||
@@ -138,18 +131,21 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
)
|
)
|
||||||
raise ServerError(msg, status_code=500)
|
raise ServerError(msg, status_code=500)
|
||||||
if 100 <= resp.status_code <= 299:
|
if 100 <= resp.status_code <= 299:
|
||||||
first_line = (
|
rbody = "".join(
|
||||||
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
|
[
|
||||||
).encode()
|
"HTTP/1.1 ",
|
||||||
rbody = bytearray(first_line)
|
str(resp.status_code),
|
||||||
rbody += (
|
" ",
|
||||||
"".join([f"{k}: {v}\r\n" for k, v in resp.headers.items()])
|
resp.reason_phrase,
|
||||||
).encode()
|
"\r\n",
|
||||||
rbody += b"\r\n"
|
]
|
||||||
|
)
|
||||||
|
rbody += "".join(f"{k}: {v}\r\n" for k, v in resp.headers.items())
|
||||||
if resp.body is not None:
|
if resp.body is not None:
|
||||||
rbody += resp.body
|
rbody += f"\r\n{resp.body}\r\n\r\n"
|
||||||
rbody += b"\r\n\r\n"
|
else:
|
||||||
await super().send(rbody)
|
rbody += "\r\n"
|
||||||
|
await super().send(rbody.encode())
|
||||||
else:
|
else:
|
||||||
raise ServerError(resp.body, resp.status_code)
|
raise ServerError(resp.body, resp.status_code)
|
||||||
self.websocket = WebsocketImplProtocol(
|
self.websocket = WebsocketImplProtocol(
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from ssl import SSLContext
|
from ssl import SSLContext
|
||||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ from functools import partial
|
|||||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||||
from signal import signal as signal_func
|
from signal import signal as signal_func
|
||||||
|
|
||||||
from sanic.application.ext import setup_ext
|
|
||||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, logger
|
||||||
from sanic.models.server_types import Signal
|
from sanic.models.server_types import Signal
|
||||||
@@ -117,7 +114,6 @@ def serve(
|
|||||||
**asyncio_server_kwargs,
|
**asyncio_server_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
setup_ext(app)
|
|
||||||
if run_async:
|
if run_async:
|
||||||
return AsyncioServer(
|
return AsyncioServer(
|
||||||
app=app,
|
app=app,
|
||||||
@@ -138,7 +134,6 @@ def serve(
|
|||||||
# Ignore SIGINT when run_multiple
|
# Ignore SIGINT when run_multiple
|
||||||
if run_multiple:
|
if run_multiple:
|
||||||
signal_func(SIGINT, SIG_IGN)
|
signal_func(SIGINT, SIG_IGN)
|
||||||
os.environ["SANIC_WORKER_PROCESS"] = "true"
|
|
||||||
|
|
||||||
# Register signals for graceful termination
|
# Register signals for graceful termination
|
||||||
if register_sys_signals:
|
if register_sys_signals:
|
||||||
@@ -178,9 +173,6 @@ def serve(
|
|||||||
loop.run_until_complete(asyncio.sleep(0.1))
|
loop.run_until_complete(asyncio.sleep(0.1))
|
||||||
start_shutdown = start_shutdown + 0.1
|
start_shutdown = start_shutdown + 0.1
|
||||||
|
|
||||||
if sys.version_info > (3, 7):
|
|
||||||
app.shutdown_tasks(graceful - start_shutdown)
|
|
||||||
|
|
||||||
# Force close non-idle connection after waiting for
|
# Force close non-idle connection after waiting for
|
||||||
# graceful_shutdown_timeout
|
# graceful_shutdown_timeout
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
@@ -189,6 +181,7 @@ def serve(
|
|||||||
else:
|
else:
|
||||||
conn.abort()
|
conn.abort()
|
||||||
loop.run_until_complete(app._server_event("shutdown", "after"))
|
loop.run_until_complete(app._server_event("shutdown", "after"))
|
||||||
|
|
||||||
remove_unix_socket(unix)
|
remove_unix_socket(unix)
|
||||||
|
|
||||||
|
|
||||||
@@ -256,10 +249,7 @@ def serve_multiple(server_settings, workers):
|
|||||||
mp = multiprocessing.get_context("fork")
|
mp = multiprocessing.get_context("fork")
|
||||||
|
|
||||||
for _ in range(workers):
|
for _ in range(workers):
|
||||||
process = mp.Process(
|
process = mp.Process(target=serve, kwargs=server_settings)
|
||||||
target=serve,
|
|
||||||
kwargs=server_settings,
|
|
||||||
)
|
|
||||||
process.daemon = True
|
process.daemon = True
|
||||||
process.start()
|
process.start()
|
||||||
processes.append(process)
|
processes.append(process)
|
||||||
|
|||||||
101
sanic/signals.py
101
sanic/signals.py
@@ -2,9 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
|
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
|
||||||
from sanic_routing.exceptions import NotFound # type: ignore
|
from sanic_routing.exceptions import NotFound # type: ignore
|
||||||
@@ -15,47 +14,29 @@ from sanic.log import error_logger, logger
|
|||||||
from sanic.models.handler_types import SignalHandler
|
from sanic.models.handler_types import SignalHandler
|
||||||
|
|
||||||
|
|
||||||
class Event(Enum):
|
|
||||||
SERVER_INIT_AFTER = "server.init.after"
|
|
||||||
SERVER_INIT_BEFORE = "server.init.before"
|
|
||||||
SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
|
|
||||||
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
|
|
||||||
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
|
|
||||||
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
|
|
||||||
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
|
|
||||||
HTTP_LIFECYCLE_HANDLE = "http.lifecycle.handle"
|
|
||||||
HTTP_LIFECYCLE_READ_BODY = "http.lifecycle.read_body"
|
|
||||||
HTTP_LIFECYCLE_READ_HEAD = "http.lifecycle.read_head"
|
|
||||||
HTTP_LIFECYCLE_REQUEST = "http.lifecycle.request"
|
|
||||||
HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
|
|
||||||
HTTP_ROUTING_AFTER = "http.routing.after"
|
|
||||||
HTTP_ROUTING_BEFORE = "http.routing.before"
|
|
||||||
HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
|
|
||||||
HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
|
|
||||||
HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
|
|
||||||
|
|
||||||
|
|
||||||
RESERVED_NAMESPACES = {
|
RESERVED_NAMESPACES = {
|
||||||
"server": (
|
"server": (
|
||||||
Event.SERVER_INIT_AFTER.value,
|
# "server.main.start",
|
||||||
Event.SERVER_INIT_BEFORE.value,
|
# "server.main.stop",
|
||||||
Event.SERVER_SHUTDOWN_AFTER.value,
|
"server.init.before",
|
||||||
Event.SERVER_SHUTDOWN_BEFORE.value,
|
"server.init.after",
|
||||||
|
"server.shutdown.before",
|
||||||
|
"server.shutdown.after",
|
||||||
),
|
),
|
||||||
"http": (
|
"http": (
|
||||||
Event.HTTP_LIFECYCLE_BEGIN.value,
|
"http.lifecycle.begin",
|
||||||
Event.HTTP_LIFECYCLE_COMPLETE.value,
|
"http.lifecycle.complete",
|
||||||
Event.HTTP_LIFECYCLE_EXCEPTION.value,
|
"http.lifecycle.exception",
|
||||||
Event.HTTP_LIFECYCLE_HANDLE.value,
|
"http.lifecycle.handle",
|
||||||
Event.HTTP_LIFECYCLE_READ_BODY.value,
|
"http.lifecycle.read_body",
|
||||||
Event.HTTP_LIFECYCLE_READ_HEAD.value,
|
"http.lifecycle.read_head",
|
||||||
Event.HTTP_LIFECYCLE_REQUEST.value,
|
"http.lifecycle.request",
|
||||||
Event.HTTP_LIFECYCLE_RESPONSE.value,
|
"http.lifecycle.response",
|
||||||
Event.HTTP_ROUTING_AFTER.value,
|
"http.routing.after",
|
||||||
Event.HTTP_ROUTING_BEFORE.value,
|
"http.routing.before",
|
||||||
Event.HTTP_LIFECYCLE_SEND.value,
|
"http.lifecycle.send",
|
||||||
Event.HTTP_MIDDLEWARE_AFTER.value,
|
"http.middleware.after",
|
||||||
Event.HTTP_MIDDLEWARE_BEFORE.value,
|
"http.middleware.before",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +113,7 @@ class SignalRouter(BaseRouter):
|
|||||||
if fail_not_found:
|
if fail_not_found:
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
|
if self.ctx.app.debug:
|
||||||
error_logger.warning(str(e))
|
error_logger.warning(str(e))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -142,21 +123,12 @@ class SignalRouter(BaseRouter):
|
|||||||
if context:
|
if context:
|
||||||
params.update(context)
|
params.update(context)
|
||||||
|
|
||||||
signals = group.routes
|
|
||||||
if not reverse:
|
if not reverse:
|
||||||
signals = signals[::-1]
|
handlers = handlers[::-1]
|
||||||
try:
|
try:
|
||||||
for signal in signals:
|
for handler in handlers:
|
||||||
params.pop("__trigger__", None)
|
if condition is None or condition == handler.__requirements__:
|
||||||
if (
|
maybe_coroutine = handler(**params)
|
||||||
(condition is None and signal.ctx.exclusive is False)
|
|
||||||
or (
|
|
||||||
condition is None
|
|
||||||
and not signal.handler.__requirements__
|
|
||||||
)
|
|
||||||
or (condition == signal.handler.__requirements__)
|
|
||||||
) and (signal.ctx.trigger or event == signal.ctx.definition):
|
|
||||||
maybe_coroutine = signal.handler(**params)
|
|
||||||
if isawaitable(maybe_coroutine):
|
if isawaitable(maybe_coroutine):
|
||||||
retval = await maybe_coroutine
|
retval = await maybe_coroutine
|
||||||
if retval:
|
if retval:
|
||||||
@@ -199,36 +171,23 @@ class SignalRouter(BaseRouter):
|
|||||||
handler: SignalHandler,
|
handler: SignalHandler,
|
||||||
event: str,
|
event: str,
|
||||||
condition: Optional[Dict[str, Any]] = None,
|
condition: Optional[Dict[str, Any]] = None,
|
||||||
exclusive: bool = True,
|
|
||||||
) -> Signal:
|
) -> Signal:
|
||||||
event_definition = event
|
|
||||||
parts = self._build_event_parts(event)
|
parts = self._build_event_parts(event)
|
||||||
if parts[2].startswith("<"):
|
if parts[2].startswith("<"):
|
||||||
name = ".".join([*parts[:-1], "*"])
|
name = ".".join([*parts[:-1], "*"])
|
||||||
trigger = self._clean_trigger(parts[2])
|
|
||||||
else:
|
else:
|
||||||
name = event
|
name = event
|
||||||
trigger = ""
|
|
||||||
|
|
||||||
if not trigger:
|
|
||||||
event = ".".join([*parts[:2], "<__trigger__>"])
|
|
||||||
|
|
||||||
handler.__requirements__ = condition # type: ignore
|
handler.__requirements__ = condition # type: ignore
|
||||||
handler.__trigger__ = trigger # type: ignore
|
|
||||||
|
|
||||||
signal = super().add(
|
return super().add(
|
||||||
event,
|
event,
|
||||||
handler,
|
handler,
|
||||||
|
requirements=condition,
|
||||||
name=name,
|
name=name,
|
||||||
append=True,
|
append=True,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
signal.ctx.exclusive = exclusive
|
|
||||||
signal.ctx.trigger = trigger
|
|
||||||
signal.ctx.definition = event_definition
|
|
||||||
|
|
||||||
return cast(Signal, signal)
|
|
||||||
|
|
||||||
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
|
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
|
||||||
self.add(_blank, "sanic.__signal__.__init__")
|
self.add(_blank, "sanic.__signal__.__init__")
|
||||||
|
|
||||||
@@ -260,9 +219,3 @@ class SignalRouter(BaseRouter):
|
|||||||
"Cannot declare reserved signal event: %s" % event
|
"Cannot declare reserved signal event: %s" % event
|
||||||
)
|
)
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
def _clean_trigger(self, trigger: str) -> str:
|
|
||||||
trigger = trigger[1:-1]
|
|
||||||
if ":" in trigger:
|
|
||||||
trigger, _ = trigger.split(":")
|
|
||||||
return trigger
|
|
||||||
|
|||||||
196
sanic/tls.py
196
sanic/tls.py
@@ -1,196 +0,0 @@
|
|||||||
import os
|
|
||||||
import ssl
|
|
||||||
|
|
||||||
from typing import Iterable, Optional, Union
|
|
||||||
|
|
||||||
from sanic.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
# Only allow secure ciphers, notably leaving out AES-CBC mode
|
|
||||||
# OpenSSL chooses ECDSA or RSA depending on the cert in use
|
|
||||||
CIPHERS_TLS12 = [
|
|
||||||
"ECDHE-ECDSA-CHACHA20-POLY1305",
|
|
||||||
"ECDHE-ECDSA-AES256-GCM-SHA384",
|
|
||||||
"ECDHE-ECDSA-AES128-GCM-SHA256",
|
|
||||||
"ECDHE-RSA-CHACHA20-POLY1305",
|
|
||||||
"ECDHE-RSA-AES256-GCM-SHA384",
|
|
||||||
"ECDHE-RSA-AES128-GCM-SHA256",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_context(
|
|
||||||
certfile: Optional[str] = None,
|
|
||||||
keyfile: Optional[str] = None,
|
|
||||||
password: Optional[str] = None,
|
|
||||||
) -> ssl.SSLContext:
|
|
||||||
"""Create a context with secure crypto and HTTP/1.1 in protocols."""
|
|
||||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
|
||||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
||||||
context.set_ciphers(":".join(CIPHERS_TLS12))
|
|
||||||
context.set_alpn_protocols(["http/1.1"])
|
|
||||||
context.sni_callback = server_name_callback
|
|
||||||
if certfile and keyfile:
|
|
||||||
context.load_cert_chain(certfile, keyfile, password)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
def shorthand_to_ctx(
|
|
||||||
ctxdef: Union[None, ssl.SSLContext, dict, str]
|
|
||||||
) -> Optional[ssl.SSLContext]:
|
|
||||||
"""Convert an ssl argument shorthand to an SSLContext object."""
|
|
||||||
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
|
|
||||||
return ctxdef
|
|
||||||
if isinstance(ctxdef, str):
|
|
||||||
return load_cert_dir(ctxdef)
|
|
||||||
if isinstance(ctxdef, dict):
|
|
||||||
return CertSimple(**ctxdef)
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid ssl argument {type(ctxdef)}."
|
|
||||||
" Expecting a list of certdirs, a dict or an SSLContext."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process_to_context(
|
|
||||||
ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
|
|
||||||
) -> Optional[ssl.SSLContext]:
|
|
||||||
"""Process app.run ssl argument from easy formats to full SSLContext."""
|
|
||||||
return (
|
|
||||||
CertSelector(map(shorthand_to_ctx, ssldef))
|
|
||||||
if isinstance(ssldef, (list, tuple))
|
|
||||||
else shorthand_to_ctx(ssldef)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_cert_dir(p: str) -> ssl.SSLContext:
|
|
||||||
if os.path.isfile(p):
|
|
||||||
raise ValueError(f"Certificate folder expected but {p} is a file.")
|
|
||||||
keyfile = os.path.join(p, "privkey.pem")
|
|
||||||
certfile = os.path.join(p, "fullchain.pem")
|
|
||||||
if not os.access(keyfile, os.R_OK):
|
|
||||||
raise ValueError(
|
|
||||||
f"Certificate not found or permission denied {keyfile}"
|
|
||||||
)
|
|
||||||
if not os.access(certfile, os.R_OK):
|
|
||||||
raise ValueError(
|
|
||||||
f"Certificate not found or permission denied {certfile}"
|
|
||||||
)
|
|
||||||
return CertSimple(certfile, keyfile)
|
|
||||||
|
|
||||||
|
|
||||||
class CertSimple(ssl.SSLContext):
|
|
||||||
"""A wrapper for creating SSLContext with a sanic attribute."""
|
|
||||||
|
|
||||||
def __new__(cls, cert, key, **kw):
|
|
||||||
# try common aliases, rename to cert/key
|
|
||||||
certfile = kw["cert"] = kw.pop("certificate", None) or cert
|
|
||||||
keyfile = kw["key"] = kw.pop("keyfile", None) or key
|
|
||||||
password = kw.pop("password", None)
|
|
||||||
if not certfile or not keyfile:
|
|
||||||
raise ValueError("SSL dict needs filenames for cert and key.")
|
|
||||||
subject = {}
|
|
||||||
if "names" not in kw:
|
|
||||||
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
|
|
||||||
kw["names"] = [
|
|
||||||
name
|
|
||||||
for t, name in cert["subjectAltName"]
|
|
||||||
if t in ["DNS", "IP Address"]
|
|
||||||
]
|
|
||||||
subject = {k: v for item in cert["subject"] for k, v in item}
|
|
||||||
self = create_context(certfile, keyfile, password)
|
|
||||||
self.__class__ = cls
|
|
||||||
self.sanic = {**subject, **kw}
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __init__(self, cert, key, **kw):
|
|
||||||
pass # Do not call super().__init__ because it is already initialized
|
|
||||||
|
|
||||||
|
|
||||||
class CertSelector(ssl.SSLContext):
|
|
||||||
"""Automatically select SSL certificate based on the hostname that the
|
|
||||||
client is trying to access, via SSL SNI. Paths to certificate folders
|
|
||||||
with privkey.pem and fullchain.pem in them should be provided, and
|
|
||||||
will be matched in the order given whenever there is a new connection.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __new__(cls, ctxs):
|
|
||||||
return super().__new__(cls)
|
|
||||||
|
|
||||||
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
|
|
||||||
super().__init__()
|
|
||||||
self.sni_callback = selector_sni_callback # type: ignore
|
|
||||||
self.sanic_select = []
|
|
||||||
self.sanic_fallback = None
|
|
||||||
all_names = []
|
|
||||||
for i, ctx in enumerate(ctxs):
|
|
||||||
if not ctx:
|
|
||||||
continue
|
|
||||||
names = dict(getattr(ctx, "sanic", {})).get("names", [])
|
|
||||||
all_names += names
|
|
||||||
self.sanic_select.append(ctx)
|
|
||||||
if i == 0:
|
|
||||||
self.sanic_fallback = ctx
|
|
||||||
if not all_names:
|
|
||||||
raise ValueError(
|
|
||||||
"No certificates with SubjectAlternativeNames found."
|
|
||||||
)
|
|
||||||
logger.info(f"Certificate vhosts: {', '.join(all_names)}")
|
|
||||||
|
|
||||||
|
|
||||||
def find_cert(self: CertSelector, server_name: str):
|
|
||||||
"""Find the first certificate that matches the given SNI.
|
|
||||||
|
|
||||||
:raises ssl.CertificateError: No matching certificate found.
|
|
||||||
:return: A matching ssl.SSLContext object if found."""
|
|
||||||
if not server_name:
|
|
||||||
if self.sanic_fallback:
|
|
||||||
return self.sanic_fallback
|
|
||||||
raise ValueError(
|
|
||||||
"The client provided no SNI to match for certificate."
|
|
||||||
)
|
|
||||||
for ctx in self.sanic_select:
|
|
||||||
if match_hostname(ctx, server_name):
|
|
||||||
return ctx
|
|
||||||
if self.sanic_fallback:
|
|
||||||
return self.sanic_fallback
|
|
||||||
raise ValueError(f"No certificate found matching hostname {server_name!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def match_hostname(
|
|
||||||
ctx: Union[ssl.SSLContext, CertSelector], hostname: str
|
|
||||||
) -> bool:
|
|
||||||
"""Match names from CertSelector against a received hostname."""
|
|
||||||
# Local certs are considered trusted, so this can be less pedantic
|
|
||||||
# and thus faster than the deprecated ssl.match_hostname function is.
|
|
||||||
names = dict(getattr(ctx, "sanic", {})).get("names", [])
|
|
||||||
hostname = hostname.lower()
|
|
||||||
for name in names:
|
|
||||||
if name.startswith("*."):
|
|
||||||
if hostname.split(".", 1)[-1] == name[2:]:
|
|
||||||
return True
|
|
||||||
elif name == hostname:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def selector_sni_callback(
|
|
||||||
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
|
|
||||||
) -> Optional[int]:
|
|
||||||
"""Select a certificate matching the SNI."""
|
|
||||||
# Call server_name_callback to store the SNI on sslobj
|
|
||||||
server_name_callback(sslobj, server_name, ctx)
|
|
||||||
# Find a new context matching the hostname
|
|
||||||
try:
|
|
||||||
sslobj.context = find_cert(ctx, server_name)
|
|
||||||
except ValueError as e:
|
|
||||||
logger.warning(f"Rejecting TLS connection: {e}")
|
|
||||||
# This would show ERR_SSL_UNRECOGNIZED_NAME_ALERT on client side if
|
|
||||||
# asyncio/uvloop did proper SSL shutdown. They don't.
|
|
||||||
return ssl.ALERT_DESCRIPTION_UNRECOGNIZED_NAME
|
|
||||||
return None # mypy complains without explicit return
|
|
||||||
|
|
||||||
|
|
||||||
def server_name_callback(
|
|
||||||
sslobj: ssl.SSLObject, server_name: str, ctx: ssl.SSLContext
|
|
||||||
) -> None:
|
|
||||||
"""Store the received SNI as sslobj.sanic_server_name."""
|
|
||||||
sslobj.sanic_server_name = server_name # type: ignore
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
from sanic.base.meta import SanicMeta
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
|
|
||||||
from .service import TouchUp
|
from .service import TouchUp
|
||||||
|
|
||||||
|
|
||||||
class TouchUpMeta(SanicMeta):
|
class TouchUpMeta(type):
|
||||||
def __new__(cls, name, bases, attrs, **kwargs):
|
def __new__(cls, name, bases, attrs, **kwargs):
|
||||||
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ class OptionalDispatchEvent(BaseScheme):
|
|||||||
raw_source = getsource(method)
|
raw_source = getsource(method)
|
||||||
src = dedent(raw_source)
|
src = dedent(raw_source)
|
||||||
tree = parse(src)
|
tree = parse(src)
|
||||||
node = RemoveDispatch(
|
node = RemoveDispatch(self._registered_events).visit(tree)
|
||||||
self._registered_events, self.app.state.verbosity
|
|
||||||
).visit(tree)
|
|
||||||
compiled_src = compile(node, method.__name__, "exec")
|
compiled_src = compile(node, method.__name__, "exec")
|
||||||
exec_locals: Dict[str, Any] = {}
|
exec_locals: Dict[str, Any] = {}
|
||||||
exec(compiled_src, module_globals, exec_locals) # nosec
|
exec(compiled_src, module_globals, exec_locals) # nosec
|
||||||
@@ -33,9 +31,8 @@ class OptionalDispatchEvent(BaseScheme):
|
|||||||
|
|
||||||
|
|
||||||
class RemoveDispatch(NodeTransformer):
|
class RemoveDispatch(NodeTransformer):
|
||||||
def __init__(self, registered_events, verbosity: int = 0) -> None:
|
def __init__(self, registered_events) -> None:
|
||||||
self._registered_events = registered_events
|
self._registered_events = registered_events
|
||||||
self._verbosity = verbosity
|
|
||||||
|
|
||||||
def visit_Expr(self, node: Expr) -> Any:
|
def visit_Expr(self, node: Expr) -> Any:
|
||||||
call = node.value
|
call = node.value
|
||||||
@@ -52,8 +49,7 @@ class RemoveDispatch(NodeTransformer):
|
|||||||
if hasattr(event, "s"):
|
if hasattr(event, "s"):
|
||||||
event_name = getattr(event, "value", event.s)
|
event_name = getattr(event, "value", event.s)
|
||||||
if self._not_registered(event_name):
|
if self._not_registered(event_name):
|
||||||
if self._verbosity >= 2:
|
logger.debug(f"Disabling event: {event_name}")
|
||||||
logger.debug(f"Disabling event: {event_name}")
|
|
||||||
return None
|
return None
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user