Merge branch 'main' of github.com:sanic-org/sanic into signal-routing
This commit is contained in:
commit
ce2bb294f9
@ -1,5 +1,7 @@
|
|||||||
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"
|
||||||
@ -8,7 +10,6 @@ exclude_patterns:
|
|||||||
- "docker/"
|
- "docker/"
|
||||||
- "docs/"
|
- "docs/"
|
||||||
- "examples/"
|
- "examples/"
|
||||||
- "hack/"
|
|
||||||
- "scripts/"
|
- "scripts/"
|
||||||
- "tests/"
|
- "tests/"
|
||||||
checks:
|
checks:
|
||||||
@ -22,3 +23,6 @@ checks:
|
|||||||
threshold: 40
|
threshold: 40
|
||||||
complex-logic:
|
complex-logic:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
method-complexity:
|
||||||
|
config:
|
||||||
|
threshold: 10
|
||||||
|
@ -3,6 +3,9 @@ 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
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -5,11 +5,13 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
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
|
||||||
|
|
||||||
|
27
.github/workflows/coverage.yml
vendored
27
.github/workflows/coverage.yml
vendored
@ -1,19 +1,20 @@
|
|||||||
name: Coverage check
|
name: Coverage check
|
||||||
# on:
|
on:
|
||||||
# push:
|
push:
|
||||||
# branches:
|
branches:
|
||||||
# - main
|
- main
|
||||||
# tags:
|
tags:
|
||||||
# - "!*" # Do not execute on tags
|
- "!*" # Do not execute on tags
|
||||||
# paths:
|
paths:
|
||||||
# - sanic/*
|
- sanic/*
|
||||||
# - tests/*
|
- tests/*
|
||||||
# pull_request:
|
pull_request:
|
||||||
# paths:
|
paths:
|
||||||
# - "!*.MD"
|
- "!*.MD"
|
||||||
on: [push, pull_request]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
3
.github/workflows/pr-bandit.yml
vendored
3
.github/workflows/pr-bandit.yml
vendored
@ -3,9 +3,11 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
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:
|
||||||
@ -16,6 +18,7 @@ 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
|
||||||
|
2
.github/workflows/pr-docs.yml
vendored
2
.github/workflows/pr-docs.yml
vendored
@ -3,9 +3,11 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
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:
|
||||||
|
2
.github/workflows/pr-linter.yml
vendored
2
.github/workflows/pr-linter.yml
vendored
@ -3,9 +3,11 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
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:
|
||||||
|
46
.github/workflows/pr-python310.yml
vendored
Normal file
46
.github/workflows/pr-python310.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: Python 3.10 Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testPy310:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# os: [ubuntu-latest, macos-latest]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- {
|
||||||
|
python-version: "3.10",
|
||||||
|
tox-env: py310,
|
||||||
|
ignore-error-flake: "false",
|
||||||
|
command-timeout: "0",
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
python-version: "3.10",
|
||||||
|
tox-env: py310-no-ext,
|
||||||
|
ignore-error-flake: "true",
|
||||||
|
command-timeout: "600000",
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
|
||||||
|
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
|
||||||
|
command-timeout: "${{ matrix.config.command-timeout }}"
|
||||||
|
test-failure-retry: "3"
|
10
.github/workflows/pr-python37.yml
vendored
10
.github/workflows/pr-python37.yml
vendored
@ -3,19 +3,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
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: false
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
# os: [ubuntu-latest, macos-latest]
|
# os: [ubuntu-latest, macos-latest]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
10
.github/workflows/pr-python38.yml
vendored
10
.github/workflows/pr-python38.yml
vendored
@ -3,19 +3,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
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: false
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
# os: [ubuntu-latest, macos-latest]
|
# os: [ubuntu-latest, macos-latest]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
10
.github/workflows/pr-python39.yml
vendored
10
.github/workflows/pr-python39.yml
vendored
@ -3,19 +3,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
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: false
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
# os: [ubuntu-latest, macos-latest]
|
# os: [ubuntu-latest, macos-latest]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
3
.github/workflows/pr-type-check.yml
vendored
3
.github/workflows/pr-type-check.yml
vendored
@ -3,9 +3,11 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
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,6 +18,7 @@ jobs:
|
|||||||
- { 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
|
||||||
|
3
.github/workflows/pr-windows.yml
vendored
3
.github/workflows/pr-windows.yml
vendored
@ -3,9 +3,11 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
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:
|
||||||
@ -15,6 +17,7 @@ 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"]
|
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
@ -657,7 +657,7 @@ Improved Documentation
|
|||||||
Version 20.6.0
|
Version 20.6.0
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
|
*Released, but unintentionally omitting PR #1880, so was replaced by 20.6.1*
|
||||||
|
|
||||||
|
|
||||||
Version 20.3.0
|
Version 20.3.0
|
||||||
@ -1090,7 +1090,7 @@ 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 requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirement of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||||
* Fix document for logging
|
* Fix document for logging
|
||||||
|
|
||||||
Version 0.8
|
Version 0.8
|
||||||
@ -1129,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 Timout log level changed to debug (Arnulfo Solís)
|
* KeepAlive Timeout log level changed to debug (Arnulfo Solís)
|
||||||
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
* 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)
|
||||||
|
13
README.rst
13
README.rst
@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
|||||||
:stub-columns: 1
|
:stub-columns: 1
|
||||||
|
|
||||||
* - Build
|
* - Build
|
||||||
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
|
- | |Py39Test| |Py38Test| |Py37Test|
|
||||||
* - Docs
|
* - Docs
|
||||||
- | |UserGuide| |Documentation|
|
- | |UserGuide| |Documentation|
|
||||||
* - Package
|
* - Package
|
||||||
@ -27,8 +27,6 @@ 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
|
||||||
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
|
|
||||||
: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
|
||||||
@ -79,6 +77,10 @@ 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
|
||||||
------------
|
------------
|
||||||
|
|
||||||
@ -162,3 +164,8 @@ Contribution
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
|
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
|
||||||
|
@ -4,12 +4,14 @@ import asyncio
|
|||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
app = Sanic()
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
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,30 +1,29 @@
|
|||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
app = Sanic()
|
from sanic import Sanic
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
@app.middleware('request')
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("request")
|
||||||
def append_request(request):
|
def append_request(request):
|
||||||
# Add new key with random value
|
request.ctx.num = randint(0, 100)
|
||||||
request['num'] = randint(0, 100)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get('/pop')
|
@app.get("/pop")
|
||||||
def pop_handler(request):
|
def pop_handler(request):
|
||||||
# Pop key from request object
|
return text(request.ctx.num)
|
||||||
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 'num' in request:
|
if hasattr(request.ctx, "num"):
|
||||||
return text('num exist in request')
|
return text("num exist in request")
|
||||||
|
|
||||||
return text('num does not exist in reqeust')
|
return text("num does not exist in request")
|
||||||
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from sanic import Sanic
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
app = Sanic()
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
def check_request_for_authorization_status(request):
|
def check_request_for_authorization_status(request):
|
||||||
@ -27,14 +29,16 @@ 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,43 +1,53 @@
|
|||||||
from sanic import Sanic, Blueprint
|
from sanic import Blueprint, Sanic
|
||||||
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(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
bp = Blueprint("bp_"+__name__)
|
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,6 +1,7 @@
|
|||||||
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(__name__)
|
||||||
blueprint = Blueprint("name", url_prefix="/my_blueprint")
|
blueprint = Blueprint("name", url_prefix="/my_blueprint")
|
||||||
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
|
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
|
||||||
|
@ -2,17 +2,20 @@ from asyncio import sleep
|
|||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(__name__, strict_slashes=True)
|
app = Sanic(__name__, 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,8 +7,10 @@ and pass in an instance of it when we create our Sanic instance. Inside this
|
|||||||
class' default handler, we can do anything including sending exceptions to
|
class' default handler, we can do anything including sending exceptions to
|
||||||
an external service.
|
an external service.
|
||||||
"""
|
"""
|
||||||
from sanic.handlers import ErrorHandler
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
@ -16,7 +18,6 @@ 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)
|
||||||
@ -38,17 +39,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.error_handler = handler
|
app = Sanic(__name__, 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,4 +1,6 @@
|
|||||||
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
|
||||||
@ -32,20 +34,40 @@ def proxy(request, path):
|
|||||||
return response.redirect(url)
|
return response.redirect(url)
|
||||||
|
|
||||||
|
|
||||||
@https.listener("main_process_start")
|
@https.main_process_start
|
||||||
async def start(app, _):
|
async def start(app, _):
|
||||||
global http
|
http_server = await http.create_server(
|
||||||
app.http_server = await http.create_server(
|
|
||||||
port=HTTP_PORT, return_asyncio_server=True
|
port=HTTP_PORT, return_asyncio_server=True
|
||||||
)
|
)
|
||||||
app.http_server.after_start()
|
app.add_task(runner(http, http_server))
|
||||||
|
app.ctx.http_server = http_server
|
||||||
|
app.ctx.http = http
|
||||||
|
|
||||||
|
|
||||||
@https.listener("main_process_stop")
|
@https.main_process_stop
|
||||||
async def stop(app, _):
|
async def stop(app, _):
|
||||||
app.http_server.before_stop()
|
await app.ctx.http_server.before_stop()
|
||||||
await app.http_server.close()
|
await app.ctx.http_server.close()
|
||||||
app.http_server.after_stop()
|
for connection in app.ctx.http_server.connections:
|
||||||
|
connection.close_if_idle()
|
||||||
|
await app.ctx.http_server.after_stop()
|
||||||
|
app.ctx.http = False
|
||||||
|
|
||||||
|
|
||||||
|
async def runner(app: Sanic, app_server: AsyncioServer):
|
||||||
|
app.is_running = True
|
||||||
|
try:
|
||||||
|
app.signalize()
|
||||||
|
app.finalize()
|
||||||
|
ErrorHandler.finalize(app.error_handler)
|
||||||
|
app_server.init = True
|
||||||
|
|
||||||
|
await app_server.before_start()
|
||||||
|
await app_server.after_start()
|
||||||
|
await app_server.serve_forever()
|
||||||
|
finally:
|
||||||
|
app.is_running = False
|
||||||
|
app.is_stopping = True
|
||||||
|
|
||||||
|
|
||||||
https.run(port=HTTPS_PORT, debug=True)
|
https.run(port=HTTPS_PORT, debug=True)
|
||||||
|
@ -1,26 +1,30 @@
|
|||||||
|
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(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
sem = None
|
sem = None
|
||||||
|
|
||||||
|
|
||||||
@app.listener('before_server_start')
|
@app.before_server_start
|
||||||
def init(sanic, loop):
|
def init(sanic, _):
|
||||||
global sem
|
global sem
|
||||||
concurrency_per_worker = 4
|
concurrency_per_worker = 4
|
||||||
sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
|
sem = asyncio.Semaphore(concurrency_per_worker)
|
||||||
|
|
||||||
|
|
||||||
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, session.get(url) as response:
|
async with sem:
|
||||||
return await response.json()
|
response = await session.get(url)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@ -28,9 +32,9 @@ async def test(request):
|
|||||||
"""
|
"""
|
||||||
Download and serve example JSON
|
Download and serve example JSON
|
||||||
"""
|
"""
|
||||||
url = "https://api.github.com/repos/channelcat/sanic"
|
url = "https://api.github.com/repos/sanic-org/sanic"
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with httpx.AsyncClient() as session:
|
||||||
response = await bounded_fetch(session, url)
|
response = await bounded_fetch(session, url)
|
||||||
return json(response)
|
return json(response)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aiotask_context as context
|
from contextvars import ContextVar
|
||||||
|
|
||||||
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 = context.get("X-Request-ID")
|
record.request_id = app.ctx.request_id.get(None) or "n/a"
|
||||||
except ValueError:
|
except AttributeError:
|
||||||
record.request_id = "n/a"
|
record.request_id = "n/a"
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -49,8 +49,7 @@ 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_id = request.id
|
request.app.ctx.request_id.set(request.id)
|
||||||
context.set("X-Request-ID", request_id)
|
|
||||||
log.info(f"Setting {request.id=}")
|
log.info(f"Setting {request.id=}")
|
||||||
|
|
||||||
|
|
||||||
@ -61,14 +60,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", context.get("X-Request-ID"))
|
log.debug("X-Request-ID: %s", 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):
|
||||||
loop.set_task_factory(context.task_factory)
|
app.ctx.request_id = ContextVar("request_id")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@ -7,10 +8,11 @@ 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.response import json
|
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
log = logging.getLogger('logdna')
|
|
||||||
|
log = logging.getLogger("logdna")
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
@ -30,10 +32,12 @@ 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(getenv("LOGDNA_API_KEY"), options=logdna_options)
|
logdna_handler = LogDNAHandler(
|
||||||
|
getenv("LOGDNA_API_KEY"), options=logdna_options
|
||||||
|
)
|
||||||
|
|
||||||
logdna = logging.getLogger(__name__)
|
logdna = logging.getLogger(__name__)
|
||||||
logdna.setLevel(logging.INFO)
|
logdna.setLevel(logging.INFO)
|
||||||
@ -49,13 +53,8 @@ def log_request(request: Request):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def default(request):
|
def default(request):
|
||||||
return json({
|
return json({"response": "I was here"})
|
||||||
"response": "I was here"
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(
|
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
|
||||||
host="0.0.0.0",
|
|
||||||
port=getenv("PORT", 8080)
|
|
||||||
)
|
|
||||||
|
@ -59,31 +59,31 @@ async def handler_stream(request):
|
|||||||
return response.stream(body)
|
return response.stream(body)
|
||||||
|
|
||||||
|
|
||||||
@app.listener("before_server_start")
|
@app.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.listener("after_server_start")
|
@app.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.listener("before_server_stop")
|
@app.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.listener("after_server_stop")
|
@app.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.middleware("request")
|
@app.on_request
|
||||||
async def print_on_request(request):
|
async def print_on_request(request):
|
||||||
print("print_on_request")
|
print("print_on_request")
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("response")
|
@app.on_response
|
||||||
async def print_on_response(request, response):
|
async def print_on_response(request, response):
|
||||||
print("print_on_response")
|
print("print_on_response")
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
from sanic import Sanic
|
|
||||||
from sanic import response
|
|
||||||
from signal import signal, SIGINT
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import uvloop
|
import uvloop
|
||||||
|
|
||||||
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -11,12 +12,19 @@ app = Sanic(__name__)
|
|||||||
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())
|
|
||||||
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
async def main():
|
||||||
loop = asyncio.get_event_loop()
|
server = await app.create_server(
|
||||||
task = asyncio.ensure_future(server)
|
port=8000, host="0.0.0.0", return_asyncio_server=True
|
||||||
signal(SIGINT, lambda s, f: loop.stop())
|
)
|
||||||
try:
|
|
||||||
loop.run_forever()
|
if server is None:
|
||||||
except:
|
return
|
||||||
loop.stop()
|
|
||||||
|
await server.startup()
|
||||||
|
await server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
|
asyncio.run(main())
|
||||||
|
@ -11,9 +11,24 @@ from sanic.server import AsyncioServer
|
|||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.listener("after_server_start")
|
@app.before_server_start
|
||||||
async def after_start_test(app, loop):
|
async def before_server_start(app, loop):
|
||||||
print("Async Server Started!")
|
print("Async Server starting")
|
||||||
|
|
||||||
|
|
||||||
|
@app.after_server_start
|
||||||
|
async def after_server_start(app, loop):
|
||||||
|
print("Async Server started")
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_server_stop
|
||||||
|
async def before_server_stop(app, loop):
|
||||||
|
print("Async Server stopping")
|
||||||
|
|
||||||
|
|
||||||
|
@app.after_server_stop
|
||||||
|
async def after_server_stop(app, loop):
|
||||||
|
print("Async Server stopped")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@ -28,20 +43,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) # type: ignore
|
server: AsyncioServer = loop.run_until_complete(serv_task)
|
||||||
server.startup()
|
loop.run_until_complete(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.
|
||||||
server.before_start()
|
loop.run_until_complete(server.before_start())
|
||||||
server.after_start()
|
loop.run_until_complete(server.after_start())
|
||||||
try:
|
try:
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
loop.stop()
|
loop.stop()
|
||||||
finally:
|
finally:
|
||||||
server.before_stop()
|
loop.run_until_complete(server.before_stop())
|
||||||
|
|
||||||
# Wait for server to close
|
# Wait for server to close
|
||||||
close_task = server.close()
|
close_task = server.close()
|
||||||
@ -50,4 +65,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()
|
||||||
server.after_stop()
|
loop.run_until_complete(server.after_stop())
|
||||||
|
@ -1,42 +1,41 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.views import HTTPMethodView
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from sanic.views import HTTPMethodView
|
||||||
|
|
||||||
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,9 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic, response
|
||||||
from sanic.log import logger as log
|
|
||||||
from sanic import response
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
|
from sanic.log import logger as log
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
@ -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,6 +31,7 @@ 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")
|
||||||
|
|
||||||
@ -42,8 +43,10 @@ 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(os.path.abspath("setup.py"),
|
return await response.file_stream(
|
||||||
chunk_size=1024)
|
os.path.abspath("setup.py"), chunk_size=1024
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
# Exceptions
|
# Exceptions
|
||||||
@ -52,14 +55,17 @@ 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({"exception": "{}".format(exception), "status": exception.status_code},
|
return response.json(
|
||||||
status=exception.status_code)
|
{"exception": str(exception), "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})
|
||||||
@ -67,38 +73,51 @@ def post_json(request):
|
|||||||
|
|
||||||
@app.route("/form")
|
@app.route("/form")
|
||||||
def post_form_json(request):
|
def post_form_json(request):
|
||||||
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
|
return response.json(
|
||||||
|
{
|
||||||
|
"received": True,
|
||||||
|
"form_data": request.form,
|
||||||
|
"test": request.form.get("test"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/query_string")
|
@app.route("/query_string")
|
||||||
def query_string(request):
|
def query_string(request):
|
||||||
return response.json({"parsed": True, "args": request.args, "url": request.url,
|
return response.json(
|
||||||
"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.listener('after_server_start')
|
@app.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.listener('before_server_stop')
|
@app.before_server_stop
|
||||||
def before_stop(app, loop):
|
def before_stop(app, loop):
|
||||||
log.info("SERVER STOPPING")
|
log.info("SERVER STOPPING")
|
||||||
|
|
||||||
|
|
||||||
@app.listener('after_server_stop')
|
@app.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,7 +1,8 @@
|
|||||||
from sanic import Sanic
|
|
||||||
from sanic import response
|
|
||||||
import socket
|
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
@ -10,8 +11,9 @@ app = Sanic(__name__)
|
|||||||
async def test(request):
|
async def test(request):
|
||||||
return response.text("OK")
|
return response.text("OK")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
server_address = './uds_socket'
|
if __name__ == "__main__":
|
||||||
|
server_address = "./uds_socket"
|
||||||
# Make sure the socket does not already exist
|
# Make sure the socket does not already exist
|
||||||
try:
|
try:
|
||||||
os.unlink(server_address)
|
os.unlink(server_address)
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic, response
|
||||||
from sanic import response
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
@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,7 +8,9 @@ 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([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
|
bpg = Blueprint.group(
|
||||||
|
bp1, bp2, url_prefix="/sentient/robot", version=1, strict_slashes=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp1.get("/name")
|
@bp1.get("/name")
|
||||||
@ -31,5 +33,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)
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import redirect
|
from sanic.response import redirect
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
FROM catthehacker/ubuntu:act-latest
|
|
||||||
SHELL [ "/bin/bash", "-c" ]
|
|
||||||
ENTRYPOINT []
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install gcc -y
|
|
||||||
RUN apt-get install -y --no-install-recommends g++
|
|
@ -1,196 +1,15 @@
|
|||||||
import os
|
from sanic.cli.app import SanicCLI
|
||||||
import sys
|
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class SanicArgumentParser(ArgumentParser):
|
if OS_IS_WINDOWS:
|
||||||
def add_bool_arguments(self, *args, **kwargs):
|
enable_windows_color_support()
|
||||||
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():
|
||||||
parser = SanicArgumentParser(
|
cli = SanicCLI()
|
||||||
prog="sanic",
|
cli.attach()
|
||||||
description=BASE_LOGO,
|
cli.run()
|
||||||
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.9.1"
|
__version__ = "21.12.0dev"
|
||||||
|
447
sanic/app.py
447
sanic/app.py
@ -3,7 +3,9 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
from asyncio import (
|
from asyncio import (
|
||||||
AbstractEventLoop,
|
AbstractEventLoop,
|
||||||
@ -16,10 +18,11 @@ from asyncio import (
|
|||||||
from asyncio.futures import Future
|
from asyncio.futures import Future
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from importlib import import_module
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from ssl import Purpose, SSLContext, create_default_context
|
from ssl import SSLContext
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import (
|
from typing import (
|
||||||
@ -39,17 +42,24 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
|
from warnings import filterwarnings, warn
|
||||||
|
|
||||||
from sanic_routing.exceptions import FinalizationError # type: ignore
|
from sanic_routing.exceptions import ( # type: ignore
|
||||||
from sanic_routing.exceptions import NotFound # type: ignore
|
FinalizationError,
|
||||||
|
NotFound,
|
||||||
|
)
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
|
|
||||||
from sanic import reloader_helpers
|
from sanic import reloader_helpers
|
||||||
|
from sanic.application.logo import get_logo
|
||||||
|
from sanic.application.motd import MOTD
|
||||||
|
from sanic.application.state import ApplicationState, Mode
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.base import BaseSanic
|
from sanic.base import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.config import BASE_LOGO, SANIC_PREFIX, Config
|
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||||
|
from sanic.config import SANIC_PREFIX, Config
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
SanicException,
|
SanicException,
|
||||||
@ -57,17 +67,20 @@ from sanic.exceptions import (
|
|||||||
URLBuildError,
|
URLBuildError,
|
||||||
)
|
)
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
|
from sanic.http import Stage
|
||||||
|
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger
|
||||||
from sanic.mixins.listeners import ListenerEvent
|
from sanic.mixins.listeners import ListenerEvent
|
||||||
from sanic.models.futures import (
|
from sanic.models.futures import (
|
||||||
FutureException,
|
FutureException,
|
||||||
FutureListener,
|
FutureListener,
|
||||||
FutureMiddleware,
|
FutureMiddleware,
|
||||||
|
FutureRegistry,
|
||||||
FutureRoute,
|
FutureRoute,
|
||||||
FutureSignal,
|
FutureSignal,
|
||||||
FutureStatic,
|
FutureStatic,
|
||||||
)
|
)
|
||||||
from sanic.models.handler_types import ListenerType, MiddlewareType
|
from sanic.models.handler_types import ListenerType, MiddlewareType
|
||||||
|
from sanic.models.handler_types import Sanic as SanicVar
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import BaseHTTPResponse, HTTPResponse
|
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
@ -77,9 +90,16 @@ from sanic.server import serve, serve_multiple, serve_single
|
|||||||
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
||||||
from sanic.server.websockets.impl import ConnectionClosed
|
from sanic.server.websockets.impl import ConnectionClosed
|
||||||
from sanic.signals import Signal, SignalRouter
|
from sanic.signals import Signal, SignalRouter
|
||||||
|
from sanic.tls import process_to_context
|
||||||
from sanic.touchup import TouchUp, TouchUpMeta
|
from sanic.touchup import TouchUp, TouchUpMeta
|
||||||
|
|
||||||
|
|
||||||
|
if OS_IS_WINDOWS:
|
||||||
|
enable_windows_color_support()
|
||||||
|
|
||||||
|
filterwarnings("once", category=DeprecationWarning)
|
||||||
|
|
||||||
|
|
||||||
class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
||||||
"""
|
"""
|
||||||
The main application instance
|
The main application instance
|
||||||
@ -92,21 +112,24 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"_run_request_middleware",
|
"_run_request_middleware",
|
||||||
)
|
)
|
||||||
__fake_slots__ = (
|
__fake_slots__ = (
|
||||||
"_asgi_app",
|
|
||||||
"_app_registry",
|
"_app_registry",
|
||||||
|
"_asgi_app",
|
||||||
"_asgi_client",
|
"_asgi_client",
|
||||||
"_blueprint_order",
|
"_blueprint_order",
|
||||||
"_delayed_tasks",
|
"_delayed_tasks",
|
||||||
"_future_routes",
|
|
||||||
"_future_statics",
|
|
||||||
"_future_middleware",
|
|
||||||
"_future_listeners",
|
|
||||||
"_future_exceptions",
|
"_future_exceptions",
|
||||||
|
"_future_listeners",
|
||||||
|
"_future_middleware",
|
||||||
|
"_future_registry",
|
||||||
|
"_future_routes",
|
||||||
"_future_signals",
|
"_future_signals",
|
||||||
|
"_future_statics",
|
||||||
|
"_state",
|
||||||
"_test_client",
|
"_test_client",
|
||||||
"_test_manager",
|
"_test_manager",
|
||||||
"auto_reload",
|
|
||||||
"asgi",
|
"asgi",
|
||||||
|
"auto_reload",
|
||||||
|
"auto_reload",
|
||||||
"blueprints",
|
"blueprints",
|
||||||
"config",
|
"config",
|
||||||
"configure_logging",
|
"configure_logging",
|
||||||
@ -120,7 +143,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"name",
|
"name",
|
||||||
"named_request_middleware",
|
"named_request_middleware",
|
||||||
"named_response_middleware",
|
"named_response_middleware",
|
||||||
"reload_dirs",
|
|
||||||
"request_class",
|
"request_class",
|
||||||
"request_middleware",
|
"request_middleware",
|
||||||
"response_middleware",
|
"response_middleware",
|
||||||
@ -157,7 +179,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
# logging
|
# logging
|
||||||
if configure_logging:
|
if configure_logging:
|
||||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
|
||||||
|
logging.config.dictConfig(dict_config) # type: ignore
|
||||||
|
|
||||||
if config and (load_env is not True or env_prefix != SANIC_PREFIX):
|
if config and (load_env is not True or env_prefix != SANIC_PREFIX):
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
@ -165,38 +188,35 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"load_env or env_prefix"
|
"load_env or env_prefix"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._asgi_client = None
|
self._asgi_client: Any = None
|
||||||
|
self._test_client: Any = None
|
||||||
|
self._test_manager: Any = None
|
||||||
self._blueprint_order: List[Blueprint] = []
|
self._blueprint_order: List[Blueprint] = []
|
||||||
self._delayed_tasks: List[str] = []
|
self._delayed_tasks: List[str] = []
|
||||||
self._test_client = None
|
self._future_registry: FutureRegistry = FutureRegistry()
|
||||||
self._test_manager = None
|
self._state: ApplicationState = ApplicationState(app=self)
|
||||||
self.asgi = False
|
|
||||||
self.auto_reload = False
|
|
||||||
self.blueprints: Dict[str, Blueprint] = {}
|
self.blueprints: Dict[str, Blueprint] = {}
|
||||||
self.config = config or Config(
|
self.config: Config = config or Config(
|
||||||
load_env=load_env, env_prefix=env_prefix
|
load_env=load_env,
|
||||||
|
env_prefix=env_prefix,
|
||||||
|
app=self,
|
||||||
)
|
)
|
||||||
self.configure_logging = configure_logging
|
self.configure_logging: bool = configure_logging
|
||||||
self.ctx = ctx or SimpleNamespace()
|
self.ctx: Any = ctx or SimpleNamespace()
|
||||||
self.debug = None
|
self.debug = False
|
||||||
self.error_handler = error_handler or ErrorHandler(
|
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
||||||
fallback=self.config.FALLBACK_ERROR_FORMAT,
|
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
||||||
)
|
|
||||||
self.is_running = False
|
|
||||||
self.is_stopping = False
|
|
||||||
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
|
|
||||||
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
self.reload_dirs: Set[Path] = set()
|
self.request_class: Type[Request] = request_class or Request
|
||||||
self.request_class = request_class
|
|
||||||
self.request_middleware: Deque[MiddlewareType] = deque()
|
self.request_middleware: Deque[MiddlewareType] = deque()
|
||||||
self.response_middleware: Deque[MiddlewareType] = deque()
|
self.response_middleware: Deque[MiddlewareType] = deque()
|
||||||
self.router = router or Router()
|
self.router: Router = router or Router()
|
||||||
self.signal_router = signal_router or SignalRouter()
|
self.signal_router: SignalRouter = signal_router or SignalRouter()
|
||||||
self.sock = None
|
self.sock: Optional[socket] = None
|
||||||
self.strict_slashes = strict_slashes
|
self.strict_slashes: bool = strict_slashes
|
||||||
self.websocket_enabled = False
|
self.websocket_enabled: bool = False
|
||||||
self.websocket_tasks: Set[Future] = set()
|
self.websocket_tasks: Set[Future[Any]] = set()
|
||||||
|
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
@ -232,7 +252,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
# Registration
|
# Registration
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def add_task(self, task) -> None:
|
def add_task(
|
||||||
|
self,
|
||||||
|
task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Schedule a task to run later, after the loop has started.
|
Schedule a task to run later, after the loop has started.
|
||||||
Different from asyncio.ensure_future in that it does not
|
Different from asyncio.ensure_future in that it does not
|
||||||
@ -255,7 +278,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
self.signal(task_name)(partial(self.run_delayed_task, task=task))
|
self.signal(task_name)(partial(self.run_delayed_task, task=task))
|
||||||
self._delayed_tasks.append(task_name)
|
self._delayed_tasks.append(task_name)
|
||||||
|
|
||||||
def register_listener(self, listener: Callable, event: str) -> Any:
|
def register_listener(
|
||||||
|
self, listener: ListenerType[SanicVar], event: str
|
||||||
|
) -> ListenerType[SanicVar]:
|
||||||
"""
|
"""
|
||||||
Register the listener for a given event.
|
Register the listener for a given event.
|
||||||
|
|
||||||
@ -281,7 +306,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
return listener
|
return listener
|
||||||
|
|
||||||
def register_middleware(self, middleware, attach_to: str = "request"):
|
def register_middleware(
|
||||||
|
self, middleware: MiddlewareType, attach_to: str = "request"
|
||||||
|
) -> MiddlewareType:
|
||||||
"""
|
"""
|
||||||
Register an application level middleware that will be attached
|
Register an application level middleware that will be attached
|
||||||
to all the API URLs registered under this application.
|
to all the API URLs registered under this application.
|
||||||
@ -307,14 +334,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
def register_named_middleware(
|
def register_named_middleware(
|
||||||
self,
|
self,
|
||||||
middleware,
|
middleware: MiddlewareType,
|
||||||
route_names: Iterable[str],
|
route_names: Iterable[str],
|
||||||
attach_to: str = "request",
|
attach_to: str = "request",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Method for attaching middleware to specific routes. This is mainly an
|
Method for attaching middleware to specific routes. This is mainly an
|
||||||
internal tool for use by Blueprints to attach middleware to only its
|
internal tool for use by Blueprints to attach middleware to only its
|
||||||
specfic routes. But, it could be used in a more generalized fashion.
|
specific routes. But, it could be used in a more generalized fashion.
|
||||||
|
|
||||||
:param middleware: the middleware to execute
|
:param middleware: the middleware to execute
|
||||||
:param route_names: a list of the names of the endpoints
|
:param route_names: a list of the names of the endpoints
|
||||||
@ -701,9 +728,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
A handler that catches specific exceptions and outputs a response.
|
A handler that catches specific exceptions and outputs a response.
|
||||||
|
|
||||||
:param request: The current request object
|
:param request: The current request object
|
||||||
:type request: :class:`SanicASGITestClient`
|
|
||||||
:param exception: The exception that was raised
|
:param exception: The exception that was raised
|
||||||
:type exception: BaseException
|
|
||||||
:raises ServerError: response 500
|
:raises ServerError: response 500
|
||||||
"""
|
"""
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
@ -712,6 +737,50 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
context={"request": request, "exception": exception},
|
context={"request": request, "exception": exception},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.stream is not None
|
||||||
|
and request.stream.stage is not Stage.HANDLER
|
||||||
|
):
|
||||||
|
error_logger.exception(exception, exc_info=True)
|
||||||
|
logger.error(
|
||||||
|
"The error response will not be sent to the client for "
|
||||||
|
f'the following exception:"{exception}". A previous response '
|
||||||
|
"has at least partially been sent."
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------- deprecated -----------------
|
||||||
|
handler = self.error_handler._lookup(
|
||||||
|
exception, request.name if request else None
|
||||||
|
)
|
||||||
|
if handler:
|
||||||
|
warn(
|
||||||
|
"An error occurred while handling the request after at "
|
||||||
|
"least some part of the response was sent to the client. "
|
||||||
|
"Therefore, the response from your custom exception "
|
||||||
|
f"handler {handler.__name__} will not be sent to the "
|
||||||
|
"client. Beginning in v22.6, Sanic will stop executing "
|
||||||
|
"custom exception handlers in this scenario. Exception "
|
||||||
|
"handlers should only be used to generate the exception "
|
||||||
|
"responses. If you would like to perform any other "
|
||||||
|
"action on a raised exception, please consider using a "
|
||||||
|
"signal handler like "
|
||||||
|
'`@app.signal("http.lifecycle.exception")`\n'
|
||||||
|
"For further information, please see the docs: "
|
||||||
|
"https://sanicframework.org/en/guide/advanced/"
|
||||||
|
"signals.html",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
response = self.error_handler.response(request, exception)
|
||||||
|
if isawaitable(response):
|
||||||
|
response = await response
|
||||||
|
except BaseException as e:
|
||||||
|
logger.error("An error occurred in the exception handler.")
|
||||||
|
error_logger.exception(e)
|
||||||
|
# ----------------------------------------------
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
@ -741,6 +810,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
if response is not None:
|
if response is not None:
|
||||||
try:
|
try:
|
||||||
|
request.reset_response()
|
||||||
response = await request.respond(response)
|
response = await request.respond(response)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
# Skip response middleware
|
# Skip response middleware
|
||||||
@ -752,6 +822,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
if request.stream:
|
if request.stream:
|
||||||
response = request.stream.response
|
response = request.stream.response
|
||||||
if isinstance(response, BaseHTTPResponse):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
|
await self.dispatch(
|
||||||
|
"http.lifecycle.response",
|
||||||
|
inline=True,
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"response": response,
|
||||||
|
},
|
||||||
|
)
|
||||||
await response.send(end_stream=True)
|
await response.send(end_stream=True)
|
||||||
else:
|
else:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
@ -842,7 +920,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
|
||||||
|
if request.responded:
|
||||||
if response is not None:
|
if response is not None:
|
||||||
|
error_logger.error(
|
||||||
|
"The response object returned by the route handler "
|
||||||
|
"will not be sent to client. The request has already "
|
||||||
|
"been responded to."
|
||||||
|
)
|
||||||
|
if request.stream is not None:
|
||||||
|
response = request.stream.response
|
||||||
|
elif response is not None:
|
||||||
response = await request.respond(response)
|
response = await request.respond(response)
|
||||||
elif not hasattr(handler, "is_websocket"):
|
elif not hasattr(handler, "is_websocket"):
|
||||||
response = request.stream.response # type: ignore
|
response = request.stream.response # type: ignore
|
||||||
@ -937,6 +1024,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
# Execution
|
# Execution
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def make_coffee(self, *args, **kwargs):
|
||||||
|
self.state.coffee = True
|
||||||
|
self.run(*args, **kwargs)
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
@ -944,7 +1035,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
*,
|
*,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
auto_reload: Optional[bool] = None,
|
auto_reload: Optional[bool] = None,
|
||||||
ssl: Union[Dict[str, str], SSLContext, None] = None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock: Optional[socket] = None,
|
sock: Optional[socket] = None,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
protocol: Optional[Type[Protocol]] = None,
|
protocol: Optional[Type[Protocol]] = None,
|
||||||
@ -952,8 +1043,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
register_sys_signals: bool = True,
|
register_sys_signals: bool = True,
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
loop: None = None,
|
loop: AbstractEventLoop = None,
|
||||||
reload_dir: Optional[Union[List[str], str]] = None,
|
reload_dir: Optional[Union[List[str], str]] = None,
|
||||||
|
noisy_exceptions: Optional[bool] = None,
|
||||||
|
motd: bool = True,
|
||||||
|
fast: bool = False,
|
||||||
|
verbosity: int = 0,
|
||||||
|
motd_display: Optional[Dict[str, str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run the HTTP Server and listen until keyboard interrupt or term
|
Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
@ -970,7 +1066,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
:type auto_relaod: bool
|
:type auto_relaod: bool
|
||||||
:param ssl: SSLContext, or location of certificate and key
|
:param ssl: SSLContext, or location of certificate and key
|
||||||
for SSL encryption of worker(s)
|
for SSL encryption of worker(s)
|
||||||
:type ssl: SSLContext or dict
|
:type ssl: str, dict, SSLContext or list
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:type sock: socket
|
:type sock: socket
|
||||||
:param workers: Number of processes received before it is respected
|
:param workers: Number of processes received before it is respected
|
||||||
@ -986,8 +1082,19 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
:type access_log: bool
|
:type access_log: bool
|
||||||
:param unix: Unix socket to listen on instead of TCP port
|
:param unix: Unix socket to listen on instead of TCP port
|
||||||
:type unix: str
|
:type unix: str
|
||||||
|
:param noisy_exceptions: Log exceptions that are normally considered
|
||||||
|
to be quiet/silent
|
||||||
|
:type noisy_exceptions: bool
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
self.state.verbosity = verbosity
|
||||||
|
|
||||||
|
if fast and workers != 1:
|
||||||
|
raise RuntimeError("You cannot use both fast=True and workers=X")
|
||||||
|
|
||||||
|
if motd_display:
|
||||||
|
self.config.MOTD_DISPLAY.update(motd_display)
|
||||||
|
|
||||||
if reload_dir:
|
if reload_dir:
|
||||||
if isinstance(reload_dir, str):
|
if isinstance(reload_dir, str):
|
||||||
reload_dir = [reload_dir]
|
reload_dir = [reload_dir]
|
||||||
@ -998,7 +1105,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f"Directory {directory} could not be located"
|
f"Directory {directory} could not be located"
|
||||||
)
|
)
|
||||||
self.reload_dirs.add(Path(directory))
|
self.state.reload_dirs.add(Path(directory))
|
||||||
|
|
||||||
if loop is not None:
|
if loop is not None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -1009,7 +1116,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if auto_reload or auto_reload is None and debug:
|
if auto_reload or auto_reload is None and debug:
|
||||||
self.auto_reload = True
|
auto_reload = True
|
||||||
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
|
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
|
||||||
return reloader_helpers.watchdog(1.0, self)
|
return reloader_helpers.watchdog(1.0, self)
|
||||||
|
|
||||||
@ -1020,9 +1127,23 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
protocol = (
|
protocol = (
|
||||||
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||||
)
|
)
|
||||||
# if access_log is passed explicitly change config.ACCESS_LOG
|
|
||||||
if access_log is not None:
|
# Set explicitly passed configuration values
|
||||||
self.config.ACCESS_LOG = access_log
|
for attribute, value in {
|
||||||
|
"ACCESS_LOG": access_log,
|
||||||
|
"AUTO_RELOAD": auto_reload,
|
||||||
|
"MOTD": motd,
|
||||||
|
"NOISY_EXCEPTIONS": noisy_exceptions,
|
||||||
|
}.items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(self.config, attribute, value)
|
||||||
|
|
||||||
|
if fast:
|
||||||
|
self.state.fast = True
|
||||||
|
try:
|
||||||
|
workers = len(os.sched_getaffinity(0))
|
||||||
|
except AttributeError:
|
||||||
|
workers = os.cpu_count() or 1
|
||||||
|
|
||||||
server_settings = self._helper(
|
server_settings = self._helper(
|
||||||
host=host,
|
host=host,
|
||||||
@ -1035,7 +1156,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
backlog=backlog,
|
backlog=backlog,
|
||||||
register_sys_signals=register_sys_signals,
|
register_sys_signals=register_sys_signals,
|
||||||
auto_reload=auto_reload,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1074,7 +1194,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
port: Optional[int] = None,
|
port: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
ssl: Union[Dict[str, str], SSLContext, None] = None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock: Optional[socket] = None,
|
sock: Optional[socket] = None,
|
||||||
protocol: Type[Protocol] = None,
|
protocol: Type[Protocol] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
@ -1082,6 +1202,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
return_asyncio_server: bool = False,
|
return_asyncio_server: bool = False,
|
||||||
asyncio_server_kwargs: Dict[str, Any] = None,
|
asyncio_server_kwargs: Dict[str, Any] = None,
|
||||||
|
noisy_exceptions: Optional[bool] = None,
|
||||||
) -> Optional[AsyncioServer]:
|
) -> Optional[AsyncioServer]:
|
||||||
"""
|
"""
|
||||||
Asynchronous version of :func:`run`.
|
Asynchronous version of :func:`run`.
|
||||||
@ -1119,6 +1240,9 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
:param asyncio_server_kwargs: key-value arguments for
|
:param asyncio_server_kwargs: key-value arguments for
|
||||||
asyncio/uvloop create_server method
|
asyncio/uvloop create_server method
|
||||||
:type asyncio_server_kwargs: dict
|
:type asyncio_server_kwargs: dict
|
||||||
|
:param noisy_exceptions: Log exceptions that are normally considered
|
||||||
|
to be quiet/silent
|
||||||
|
:type noisy_exceptions: bool
|
||||||
:return: AsyncioServer if return_asyncio_server is true, else Nothing
|
:return: AsyncioServer if return_asyncio_server is true, else Nothing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -1129,10 +1253,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
protocol = (
|
protocol = (
|
||||||
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||||
)
|
)
|
||||||
|
|
||||||
# if access_log is passed explicitly change config.ACCESS_LOG
|
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||||
if access_log is not None:
|
if access_log is not None:
|
||||||
self.config.ACCESS_LOG = access_log
|
self.config.ACCESS_LOG = access_log
|
||||||
|
|
||||||
|
if noisy_exceptions is not None:
|
||||||
|
self.config.NOISY_EXCEPTIONS = noisy_exceptions
|
||||||
|
|
||||||
server_settings = self._helper(
|
server_settings = self._helper(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
@ -1243,31 +1371,20 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
def _helper(
|
def _helper(
|
||||||
self,
|
self,
|
||||||
host=None,
|
host: Optional[str] = None,
|
||||||
port=None,
|
port: Optional[int] = None,
|
||||||
debug=False,
|
debug: bool = False,
|
||||||
ssl=None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock=None,
|
sock: Optional[socket] = None,
|
||||||
unix=None,
|
unix: Optional[str] = None,
|
||||||
workers=1,
|
workers: int = 1,
|
||||||
loop=None,
|
loop: AbstractEventLoop = None,
|
||||||
protocol=HttpProtocol,
|
protocol: Type[Protocol] = HttpProtocol,
|
||||||
backlog=100,
|
backlog: int = 100,
|
||||||
register_sys_signals=True,
|
register_sys_signals: bool = True,
|
||||||
run_async=False,
|
run_async: bool = False,
|
||||||
auto_reload=False,
|
|
||||||
):
|
):
|
||||||
"""Helper function used by `run` and `create_server`."""
|
"""Helper function used by `run` and `create_server`."""
|
||||||
|
|
||||||
if isinstance(ssl, dict):
|
|
||||||
# try common aliaseses
|
|
||||||
cert = ssl.get("cert") or ssl.get("certificate")
|
|
||||||
key = ssl.get("key") or ssl.get("keyfile")
|
|
||||||
if cert is None or key is None:
|
|
||||||
raise ValueError("SSLContext or certificate and key required.")
|
|
||||||
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
|
|
||||||
context.load_cert_chain(cert, keyfile=key)
|
|
||||||
ssl = context
|
|
||||||
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"PROXIES_COUNT cannot be negative. "
|
"PROXIES_COUNT cannot be negative. "
|
||||||
@ -1275,8 +1392,26 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"#proxy-configuration"
|
"#proxy-configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.error_handler.debug = debug
|
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
self.state.host = host
|
||||||
|
self.state.port = port
|
||||||
|
self.state.workers = workers
|
||||||
|
|
||||||
|
# Serve
|
||||||
|
serve_location = ""
|
||||||
|
proto = "http"
|
||||||
|
if ssl is not None:
|
||||||
|
proto = "https"
|
||||||
|
if unix:
|
||||||
|
serve_location = f"{unix} {proto}://..."
|
||||||
|
elif sock:
|
||||||
|
serve_location = f"{sock.getsockname()} {proto}://..."
|
||||||
|
elif host and port:
|
||||||
|
# colon(:) is legal for a host only in an ipv6 address
|
||||||
|
display_host = f"[{host}]" if ":" in host else host
|
||||||
|
serve_location = f"{proto}://{display_host}:{port}"
|
||||||
|
|
||||||
|
ssl = process_to_context(ssl)
|
||||||
|
|
||||||
server_settings = {
|
server_settings = {
|
||||||
"protocol": protocol,
|
"protocol": protocol,
|
||||||
@ -1292,8 +1427,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"backlog": backlog,
|
"backlog": backlog,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Register start/stop events
|
self.motd(serve_location)
|
||||||
|
|
||||||
|
if sys.stdout.isatty() and not self.state.is_debug:
|
||||||
|
error_logger.warning(
|
||||||
|
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
|
||||||
|
"Consider using '--debug' or '--dev' while actively "
|
||||||
|
f"developing your application.{Colors.END}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register start/stop events
|
||||||
for event_name, settings_name, reverse in (
|
for event_name, settings_name, reverse in (
|
||||||
("main_process_start", "main_start", False),
|
("main_process_start", "main_start", False),
|
||||||
("main_process_stop", "main_stop", True),
|
("main_process_stop", "main_stop", True),
|
||||||
@ -1303,39 +1446,11 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
listeners.reverse()
|
listeners.reverse()
|
||||||
# Prepend sanic to the arguments when listeners are triggered
|
# Prepend sanic to the arguments when listeners are triggered
|
||||||
listeners = [partial(listener, self) for listener in listeners]
|
listeners = [partial(listener, self) for listener in listeners]
|
||||||
server_settings[settings_name] = listeners
|
server_settings[settings_name] = listeners # type: ignore
|
||||||
|
|
||||||
if self.configure_logging and debug:
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.config.LOGO
|
|
||||||
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
|
|
||||||
):
|
|
||||||
logger.debug(
|
|
||||||
self.config.LOGO
|
|
||||||
if isinstance(self.config.LOGO, str)
|
|
||||||
else BASE_LOGO
|
|
||||||
)
|
|
||||||
|
|
||||||
if run_async:
|
if run_async:
|
||||||
server_settings["run_async"] = True
|
server_settings["run_async"] = True
|
||||||
|
|
||||||
# Serve
|
|
||||||
if host and port:
|
|
||||||
proto = "http"
|
|
||||||
if ssl is not None:
|
|
||||||
proto = "https"
|
|
||||||
if unix:
|
|
||||||
logger.info(f"Goin' Fast @ {unix} {proto}://...")
|
|
||||||
else:
|
|
||||||
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
|
|
||||||
|
|
||||||
debug_mode = "enabled" if self.debug else "disabled"
|
|
||||||
reload_mode = "enabled" if auto_reload else "disabled"
|
|
||||||
logger.debug(f"Sanic auto-reload: {reload_mode}")
|
|
||||||
logger.debug(f"Sanic debug mode: {debug_mode}")
|
|
||||||
|
|
||||||
return server_settings
|
return server_settings
|
||||||
|
|
||||||
def _build_endpoint_name(self, *parts):
|
def _build_endpoint_name(self, *parts):
|
||||||
@ -1392,6 +1507,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
details: https://asgi.readthedocs.io/en/latest
|
details: https://asgi.readthedocs.io/en/latest
|
||||||
"""
|
"""
|
||||||
self.asgi = True
|
self.asgi = True
|
||||||
|
self.motd("")
|
||||||
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
|
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||||
asgi_app = self._asgi_app
|
asgi_app = self._asgi_app
|
||||||
await asgi_app()
|
await asgi_app()
|
||||||
@ -1412,6 +1528,114 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
self.config.update_config(config)
|
self.config.update_config(config)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def asgi(self):
|
||||||
|
return self.state.asgi
|
||||||
|
|
||||||
|
@asgi.setter
|
||||||
|
def asgi(self, value: bool):
|
||||||
|
self.state.asgi = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def debug(self):
|
||||||
|
return self.state.is_debug
|
||||||
|
|
||||||
|
@debug.setter
|
||||||
|
def debug(self, value: bool):
|
||||||
|
mode = Mode.DEBUG if value else Mode.PRODUCTION
|
||||||
|
self.state.mode = mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto_reload(self):
|
||||||
|
return self.config.AUTO_RELOAD
|
||||||
|
|
||||||
|
@auto_reload.setter
|
||||||
|
def auto_reload(self, value: bool):
|
||||||
|
self.config.AUTO_RELOAD = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self):
|
||||||
|
return self.state.is_running
|
||||||
|
|
||||||
|
@is_running.setter
|
||||||
|
def is_running(self, value: bool):
|
||||||
|
self.state.is_running = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_stopping(self):
|
||||||
|
return self.state.is_stopping
|
||||||
|
|
||||||
|
@is_stopping.setter
|
||||||
|
def is_stopping(self, value: bool):
|
||||||
|
self.state.is_stopping = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reload_dirs(self):
|
||||||
|
return self.state.reload_dirs
|
||||||
|
|
||||||
|
def motd(self, serve_location):
|
||||||
|
if self.config.MOTD:
|
||||||
|
mode = [f"{self.state.mode},"]
|
||||||
|
if self.state.fast:
|
||||||
|
mode.append("goin' fast")
|
||||||
|
if self.state.asgi:
|
||||||
|
mode.append("ASGI")
|
||||||
|
else:
|
||||||
|
if self.state.workers == 1:
|
||||||
|
mode.append("single worker")
|
||||||
|
else:
|
||||||
|
mode.append(f"w/ {self.state.workers} workers")
|
||||||
|
|
||||||
|
display = {
|
||||||
|
"mode": " ".join(mode),
|
||||||
|
"server": self.state.server,
|
||||||
|
"python": platform.python_version(),
|
||||||
|
"platform": platform.platform(),
|
||||||
|
}
|
||||||
|
extra = {}
|
||||||
|
if self.config.AUTO_RELOAD:
|
||||||
|
reload_display = "enabled"
|
||||||
|
if self.state.reload_dirs:
|
||||||
|
reload_display += ", ".join(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
*(
|
||||||
|
str(path.absolute())
|
||||||
|
for path in self.state.reload_dirs
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
display["auto-reload"] = reload_display
|
||||||
|
|
||||||
|
packages = []
|
||||||
|
for package_name, module_name in {
|
||||||
|
"sanic-routing": "sanic_routing",
|
||||||
|
"sanic-testing": "sanic_testing",
|
||||||
|
"sanic-ext": "sanic_ext",
|
||||||
|
}.items():
|
||||||
|
try:
|
||||||
|
module = import_module(module_name)
|
||||||
|
packages.append(f"{package_name}=={module.__version__}")
|
||||||
|
except ImportError:
|
||||||
|
...
|
||||||
|
|
||||||
|
if packages:
|
||||||
|
display["packages"] = ", ".join(packages)
|
||||||
|
|
||||||
|
if self.config.MOTD_DISPLAY:
|
||||||
|
extra.update(self.config.MOTD_DISPLAY)
|
||||||
|
|
||||||
|
logo = (
|
||||||
|
get_logo(coffee=self.state.coffee)
|
||||||
|
if self.config.LOGO == "" or self.config.LOGO is True
|
||||||
|
else self.config.LOGO
|
||||||
|
)
|
||||||
|
MOTD.output(logo, serve_location, display, extra)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Class methods
|
# Class methods
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
@ -1472,10 +1696,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def _startup(self):
|
async def _startup(self):
|
||||||
|
self._future_registry.clear()
|
||||||
self.signalize()
|
self.signalize()
|
||||||
self.finalize()
|
self.finalize()
|
||||||
ErrorHandler.finalize(self.error_handler)
|
ErrorHandler.finalize(
|
||||||
|
self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT
|
||||||
|
)
|
||||||
TouchUp.run(self)
|
TouchUp.run(self)
|
||||||
|
self.state.is_started = True
|
||||||
|
|
||||||
async def _server_event(
|
async def _server_event(
|
||||||
self,
|
self,
|
||||||
@ -1489,6 +1717,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
"shutdown",
|
"shutdown",
|
||||||
):
|
):
|
||||||
raise SanicException(f"Invalid server event: {event}")
|
raise SanicException(f"Invalid server event: {event}")
|
||||||
|
if self.state.verbosity >= 1:
|
||||||
logger.debug(f"Triggering server events: {event}")
|
logger.debug(f"Triggering server events: {event}")
|
||||||
reverse = concern == "shutdown"
|
reverse = concern == "shutdown"
|
||||||
if loop is None:
|
if loop is None:
|
||||||
|
0
sanic/application/__init__.py
Normal file
0
sanic/application/__init__.py
Normal file
57
sanic/application/logo.py
Normal file
57
sanic/application/logo.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
|
||||||
|
BASE_LOGO = """
|
||||||
|
|
||||||
|
Sanic
|
||||||
|
Build Fast. Run Fast.
|
||||||
|
|
||||||
|
"""
|
||||||
|
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ▄████████▄ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ██ ██▀▀▄ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ███████████ █ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ███████████▄▄▀ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ▀███████▀ \033[0m
|
||||||
|
\033[48;2;255;13;104m \033[0m
|
||||||
|
Dark roast. No sugar."""
|
||||||
|
|
||||||
|
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ▄███ █████ ██ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ▀███████ ███▄ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
||||||
|
\033[38;2;255;255;255;48;2;255;13;104m ████ ████████▀ \033[0m
|
||||||
|
\033[48;2;255;13;104m \033[0m
|
||||||
|
Build Fast. Run Fast."""
|
||||||
|
|
||||||
|
FULL_COLOR_LOGO = """
|
||||||
|
|
||||||
|
\033[38;2;255;13;104m ▄███ █████ ██ \033[0m ▄█▄ ██ █ █ ▄██████████
|
||||||
|
\033[38;2;255;13;104m ██ \033[0m █ █ █ ██ █ █ ██
|
||||||
|
\033[38;2;255;13;104m ▀███████ ███▄ \033[0m ▀ █ █ ██ ▄ █ ██
|
||||||
|
\033[38;2;255;13;104m ██\033[0m █████████ █ ██ █ █ ▄▄
|
||||||
|
\033[38;2;255;13;104m ████ ████████▀ \033[0m █ █ █ ██ █ ▀██ ███████
|
||||||
|
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||||
|
|
||||||
|
|
||||||
|
def get_logo(full=False, coffee=False):
|
||||||
|
logo = (
|
||||||
|
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
|
||||||
|
if sys.stdout.isatty()
|
||||||
|
else BASE_LOGO
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
sys.platform == "darwin"
|
||||||
|
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
|
||||||
|
):
|
||||||
|
logo = ansi_pattern.sub("", logo)
|
||||||
|
|
||||||
|
return logo
|
146
sanic/application/motd.py
Normal file
146
sanic/application/motd.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from shutil import get_terminal_size
|
||||||
|
from textwrap import indent, wrap
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from sanic import __version__
|
||||||
|
from sanic.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
class MOTD(ABC):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
logo: Optional[str],
|
||||||
|
serve_location: str,
|
||||||
|
data: Dict[str, str],
|
||||||
|
extra: Dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
self.logo = logo
|
||||||
|
self.serve_location = serve_location
|
||||||
|
self.data = data
|
||||||
|
self.extra = extra
|
||||||
|
self.key_width = 0
|
||||||
|
self.value_width = 0
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def display(self):
|
||||||
|
... # noqa
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def output(
|
||||||
|
cls,
|
||||||
|
logo: Optional[str],
|
||||||
|
serve_location: str,
|
||||||
|
data: Dict[str, str],
|
||||||
|
extra: Dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
|
||||||
|
motd_class(logo, serve_location, data, extra).display()
|
||||||
|
|
||||||
|
|
||||||
|
class MOTDBasic(MOTD):
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
if self.logo:
|
||||||
|
logger.debug(self.logo)
|
||||||
|
lines = [f"Sanic v{__version__}"]
|
||||||
|
if self.serve_location:
|
||||||
|
lines.append(f"Goin' Fast @ {self.serve_location}")
|
||||||
|
lines += [
|
||||||
|
*(f"{key}: {value}" for key, value in self.data.items()),
|
||||||
|
*(f"{key}: {value}" for key, value in self.extra.items()),
|
||||||
|
]
|
||||||
|
for line in lines:
|
||||||
|
logger.info(line)
|
||||||
|
|
||||||
|
|
||||||
|
class MOTDTTY(MOTD):
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.set_variables()
|
||||||
|
|
||||||
|
def set_variables(self): # no cov
|
||||||
|
fallback = (108, 24)
|
||||||
|
terminal_width = max(
|
||||||
|
get_terminal_size(fallback=fallback).columns, fallback[0]
|
||||||
|
)
|
||||||
|
self.max_value_width = terminal_width - fallback[0] + 36
|
||||||
|
|
||||||
|
self.key_width = 4
|
||||||
|
self.value_width = self.max_value_width
|
||||||
|
if self.data:
|
||||||
|
self.key_width = max(map(len, self.data.keys()))
|
||||||
|
self.value_width = min(
|
||||||
|
max(map(len, self.data.values())), self.max_value_width
|
||||||
|
)
|
||||||
|
self.logo_lines = self.logo.split("\n") if self.logo else []
|
||||||
|
self.logo_line_length = 24
|
||||||
|
self.centering_length = (
|
||||||
|
self.key_width + self.value_width + 2 + self.logo_line_length
|
||||||
|
)
|
||||||
|
self.display_length = self.key_width + self.value_width + 2
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
version = f"Sanic v{__version__}".center(self.centering_length)
|
||||||
|
running = (
|
||||||
|
f"Goin' Fast @ {self.serve_location}"
|
||||||
|
if self.serve_location
|
||||||
|
else ""
|
||||||
|
).center(self.centering_length)
|
||||||
|
length = len(version) + 2 - self.logo_line_length
|
||||||
|
first_filler = "─" * (self.logo_line_length - 1)
|
||||||
|
second_filler = "─" * length
|
||||||
|
display_filler = "─" * (self.display_length + 2)
|
||||||
|
lines = [
|
||||||
|
f"\n┌{first_filler}─{second_filler}┐",
|
||||||
|
f"│ {version} │",
|
||||||
|
f"│ {running} │",
|
||||||
|
f"├{first_filler}┬{second_filler}┤",
|
||||||
|
]
|
||||||
|
|
||||||
|
self._render_data(lines, self.data, 0)
|
||||||
|
if self.extra:
|
||||||
|
logo_part = self._get_logo_part(len(lines) - 4)
|
||||||
|
lines.append(f"| {logo_part} ├{display_filler}┤")
|
||||||
|
self._render_data(lines, self.extra, len(lines) - 4)
|
||||||
|
|
||||||
|
self._render_fill(lines)
|
||||||
|
|
||||||
|
lines.append(f"└{first_filler}┴{second_filler}┘\n")
|
||||||
|
logger.info(indent("\n".join(lines), " "))
|
||||||
|
|
||||||
|
def _render_data(self, lines, data, start):
|
||||||
|
offset = 0
|
||||||
|
for idx, (key, value) in enumerate(data.items(), start=start):
|
||||||
|
key = key.rjust(self.key_width)
|
||||||
|
|
||||||
|
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
|
||||||
|
for wrap_index, part in enumerate(wrapped):
|
||||||
|
part = part.ljust(self.value_width)
|
||||||
|
logo_part = self._get_logo_part(idx + offset + wrap_index)
|
||||||
|
display = (
|
||||||
|
f"{key}: {part}"
|
||||||
|
if wrap_index == 0
|
||||||
|
else (" " * len(key) + f" {part}")
|
||||||
|
)
|
||||||
|
lines.append(f"│ {logo_part} │ {display} │")
|
||||||
|
if wrap_index:
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
def _render_fill(self, lines):
|
||||||
|
filler = " " * self.display_length
|
||||||
|
idx = len(lines) - 5
|
||||||
|
for i in range(1, len(self.logo_lines) - idx):
|
||||||
|
logo_part = self.logo_lines[idx + i]
|
||||||
|
lines.append(f"│ {logo_part} │ {filler} │")
|
||||||
|
|
||||||
|
def _get_logo_part(self, idx):
|
||||||
|
try:
|
||||||
|
logo_part = self.logo_lines[idx]
|
||||||
|
except IndexError:
|
||||||
|
logo_part = " " * (self.logo_line_length - 3)
|
||||||
|
return logo_part
|
74
sanic/application/state.py
Normal file
74
sanic/application/state.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum, auto
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any, Set, Union
|
||||||
|
|
||||||
|
from sanic.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
|
class StrEnum(str, Enum):
|
||||||
|
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
||||||
|
return name.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class Server(StrEnum):
|
||||||
|
SANIC = auto()
|
||||||
|
ASGI = auto()
|
||||||
|
GUNICORN = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Mode(StrEnum):
|
||||||
|
PRODUCTION = auto()
|
||||||
|
DEBUG = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ApplicationState:
|
||||||
|
app: Sanic
|
||||||
|
asgi: bool = field(default=False)
|
||||||
|
coffee: bool = field(default=False)
|
||||||
|
fast: bool = field(default=False)
|
||||||
|
host: str = field(default="")
|
||||||
|
mode: Mode = field(default=Mode.PRODUCTION)
|
||||||
|
port: int = field(default=0)
|
||||||
|
reload_dirs: Set[Path] = field(default_factory=set)
|
||||||
|
server: Server = field(default=Server.SANIC)
|
||||||
|
is_running: bool = field(default=False)
|
||||||
|
is_started: bool = field(default=False)
|
||||||
|
is_stopping: bool = field(default=False)
|
||||||
|
verbosity: int = field(default=0)
|
||||||
|
workers: int = field(default=0)
|
||||||
|
|
||||||
|
# This property relates to the ApplicationState instance and should
|
||||||
|
# not be changed except in the __post_init__ method
|
||||||
|
_init: bool = field(default=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._init = True
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
|
if self._init and name == "_init":
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot change the value of _init after instantiation"
|
||||||
|
)
|
||||||
|
super().__setattr__(name, value)
|
||||||
|
if self._init and hasattr(self, f"set_{name}"):
|
||||||
|
getattr(self, f"set_{name}")(value)
|
||||||
|
|
||||||
|
def set_mode(self, value: Union[str, Mode]):
|
||||||
|
if hasattr(self.app, "error_handler"):
|
||||||
|
self.app.error_handler.debug = self.app.debug
|
||||||
|
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_debug(self):
|
||||||
|
return self.mode is Mode.DEBUG
|
@ -7,8 +7,10 @@ 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.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
|
||||||
|
|
||||||
@ -83,6 +85,8 @@ 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
|
||||||
@ -95,6 +99,8 @@ 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(
|
||||||
@ -149,6 +155,8 @@ 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):
|
||||||
@ -163,11 +171,17 @@ class ASGIApp:
|
|||||||
if data:
|
if data:
|
||||||
yield data
|
yield data
|
||||||
|
|
||||||
def respond(self, response):
|
def respond(self, response: BaseHTTPResponse):
|
||||||
|
if self.stage is not Stage.HANDLER:
|
||||||
|
self.stage = Stage.FAILED
|
||||||
|
raise RuntimeError("Response already started")
|
||||||
|
if self.response is not None:
|
||||||
|
self.response.stream = None
|
||||||
response.stream, self.response = self, response
|
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(
|
||||||
@ -195,6 +209,7 @@ 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)
|
||||||
|
@ -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(
|
||||||
@ -23,7 +23,7 @@ class BaseSanic(
|
|||||||
):
|
):
|
||||||
__fake_slots__: Tuple[str, ...]
|
__fake_slots__: Tuple[str, ...]
|
||||||
|
|
||||||
def __init__(self, name: str = None, *args, **kwargs) -> None:
|
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
|
@ -4,8 +4,22 @@ 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 TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
from sanic_routing.exceptions import NotFound # type: ignore
|
from sanic_routing.exceptions import NotFound # type: ignore
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
@ -26,6 +40,32 @@ if TYPE_CHECKING:
|
|||||||
from sanic import Sanic # noqa
|
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):
|
||||||
"""
|
"""
|
||||||
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
||||||
@ -39,7 +79,7 @@ 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 of FQDN for the sanic server to use.
|
:param host: IP Address or FQDN for the sanic server to use.
|
||||||
:param version: Blueprint Version
|
:param 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 */*
|
||||||
@ -72,7 +112,7 @@ class Blueprint(BaseSanic):
|
|||||||
self,
|
self,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
url_prefix: Optional[str] = None,
|
url_prefix: Optional[str] = None,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[List[str], 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",
|
||||||
@ -115,34 +155,21 @@ class Blueprint(BaseSanic):
|
|||||||
)
|
)
|
||||||
return self._apps
|
return self._apps
|
||||||
|
|
||||||
def route(self, *args, **kwargs):
|
@property
|
||||||
kwargs["apply"] = False
|
def registered(self) -> bool:
|
||||||
return super().route(*args, **kwargs)
|
return bool(self._apps)
|
||||||
|
|
||||||
def static(self, *args, **kwargs):
|
exception = lazy(BaseSanic.exception)
|
||||||
kwargs["apply"] = False
|
listener = lazy(BaseSanic.listener)
|
||||||
return super().static(*args, **kwargs)
|
middleware = lazy(BaseSanic.middleware)
|
||||||
|
route = lazy(BaseSanic.route)
|
||||||
def middleware(self, *args, **kwargs):
|
signal = lazy(BaseSanic.signal)
|
||||||
kwargs["apply"] = False
|
static = lazy(BaseSanic.static, as_decorator=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]] = {}
|
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
|
||||||
self.middlewares: List[MiddlewareType] = []
|
self.middlewares: List[MiddlewareType] = []
|
||||||
self.routes: List[Route] = []
|
self.routes: List[Route] = []
|
||||||
self.statics: List[RouteHandler] = []
|
self.statics: List[RouteHandler] = []
|
||||||
@ -221,7 +248,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.
|
||||||
@ -274,6 +301,7 @@ 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:
|
||||||
@ -300,12 +328,15 @@ 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,
|
||||||
future.host or self.host,
|
host,
|
||||||
strict_slashes,
|
strict_slashes,
|
||||||
future.stream,
|
future.stream,
|
||||||
version,
|
version,
|
||||||
@ -319,6 +350,10 @@ class Blueprint(BaseSanic):
|
|||||||
error_format,
|
error_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
@ -330,6 +365,11 @@ 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)
|
||||||
|
|
||||||
@ -338,30 +378,51 @@ 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 listener in self._future_listeners:
|
for future in self._future_listeners:
|
||||||
listeners[listener.event].append(app._apply_listener(listener))
|
if (self, future) in app._future_registry:
|
||||||
|
continue
|
||||||
|
listeners[future.event].append(app._apply_listener(future))
|
||||||
|
|
||||||
# Signals
|
# Signals
|
||||||
for signal in self._future_signals:
|
for future in self._future_signals:
|
||||||
signal.condition.update({"blueprint": self.name})
|
if (self, future) in app._future_registry:
|
||||||
app._apply_signal(signal)
|
continue
|
||||||
|
future.condition.update({"blueprint": self.name})
|
||||||
|
app._apply_signal(future)
|
||||||
|
|
||||||
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 = dict(listeners)
|
self.listeners.update(dict(listeners))
|
||||||
|
|
||||||
|
if self.registered:
|
||||||
|
self.register_futures(
|
||||||
|
self.apps,
|
||||||
|
self,
|
||||||
|
chain(
|
||||||
|
registered,
|
||||||
|
self._future_middleware,
|
||||||
|
self._future_exceptions,
|
||||||
|
self._future_listeners,
|
||||||
|
self._future_signals,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
async def dispatch(self, *args, **kwargs):
|
async def dispatch(self, *args, **kwargs):
|
||||||
condition = kwargs.pop("condition", {})
|
condition = kwargs.pop("condition", {})
|
||||||
@ -393,3 +454,10 @@ 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))
|
||||||
|
0
sanic/cli/__init__.py
Normal file
0
sanic/cli/__init__.py
Normal file
189
sanic/cli/app.py
Normal file
189
sanic/cli/app.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
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
|
237
sanic/cli/arguments.py
Normal file
237
sanic/cli/arguments.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from argparse import ArgumentParser, _ArgumentGroup
|
||||||
|
from typing import List, Optional, Type, Union
|
||||||
|
|
||||||
|
from sanic_routing import __version__ as __routing_version__ # type: ignore
|
||||||
|
|
||||||
|
from sanic import __version__
|
||||||
|
|
||||||
|
|
||||||
|
class Group:
|
||||||
|
name: Optional[str]
|
||||||
|
container: Union[ArgumentParser, _ArgumentGroup]
|
||||||
|
_registry: List[Type[Group]] = []
|
||||||
|
|
||||||
|
def __init_subclass__(cls) -> None:
|
||||||
|
Group._registry.append(cls)
|
||||||
|
|
||||||
|
def __init__(self, parser: ArgumentParser, title: Optional[str]):
|
||||||
|
self.parser = parser
|
||||||
|
|
||||||
|
if title:
|
||||||
|
self.container = self.parser.add_argument_group(title=f" {title}")
|
||||||
|
else:
|
||||||
|
self.container = self.parser
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, parser: ArgumentParser):
|
||||||
|
instance = cls(parser, cls.name)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def add_bool_arguments(self, *args, **kwargs):
|
||||||
|
group = self.container.add_mutually_exclusive_group()
|
||||||
|
kwargs["help"] = kwargs["help"].capitalize()
|
||||||
|
group.add_argument(*args, action="store_true", **kwargs)
|
||||||
|
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
|
||||||
|
group.add_argument(
|
||||||
|
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralGroup(Group):
|
||||||
|
name = None
|
||||||
|
|
||||||
|
def attach(self):
|
||||||
|
self.container.add_argument(
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=f"Sanic {__version__}; Routing {__routing_version__}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.container.add_argument(
|
||||||
|
"module",
|
||||||
|
help=(
|
||||||
|
"Path to your Sanic app. Example: path.to.server:app\n"
|
||||||
|
"If running a Simple Server, path to directory to serve. "
|
||||||
|
"Example: ./\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationGroup(Group):
|
||||||
|
name = "Application"
|
||||||
|
|
||||||
|
def attach(self):
|
||||||
|
self.container.add_argument(
|
||||||
|
"--factory",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Treat app as an application factory, "
|
||||||
|
"i.e. a () -> <Sanic app> callable"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"-s",
|
||||||
|
"--simple",
|
||||||
|
dest="simple",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Run Sanic as a Simple Server, and serve the contents of "
|
||||||
|
"a directory\n(module arg should be a path)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SocketGroup(Group):
|
||||||
|
name = "Socket binding"
|
||||||
|
|
||||||
|
def attach(self):
|
||||||
|
self.container.add_argument(
|
||||||
|
"-H",
|
||||||
|
"--host",
|
||||||
|
dest="host",
|
||||||
|
type=str,
|
||||||
|
default="127.0.0.1",
|
||||||
|
help="Host address [default 127.0.0.1]",
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--port",
|
||||||
|
dest="port",
|
||||||
|
type=int,
|
||||||
|
default=8000,
|
||||||
|
help="Port to serve on [default 8000]",
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"-u",
|
||||||
|
"--unix",
|
||||||
|
dest="unix",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="location of unix socket",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TLSGroup(Group):
|
||||||
|
name = "TLS certificate"
|
||||||
|
|
||||||
|
def attach(self):
|
||||||
|
self.container.add_argument(
|
||||||
|
"--cert",
|
||||||
|
dest="cert",
|
||||||
|
type=str,
|
||||||
|
help="Location of fullchain.pem, bundle.crt or equivalent",
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"--key",
|
||||||
|
dest="key",
|
||||||
|
type=str,
|
||||||
|
help="Location of privkey.pem or equivalent .key file",
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"--tls",
|
||||||
|
metavar="DIR",
|
||||||
|
type=str,
|
||||||
|
action="append",
|
||||||
|
help=(
|
||||||
|
"TLS certificate folder with fullchain.pem and privkey.pem\n"
|
||||||
|
"May be specified multiple times to choose multiple "
|
||||||
|
"certificates"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"--tls-strict-host",
|
||||||
|
dest="tlshost",
|
||||||
|
action="store_true",
|
||||||
|
help="Only allow clients that send an SNI matching server certs",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerGroup(Group):
|
||||||
|
name = "Worker"
|
||||||
|
|
||||||
|
def attach(self):
|
||||||
|
group = self.container.add_mutually_exclusive_group()
|
||||||
|
group.add_argument(
|
||||||
|
"-w",
|
||||||
|
"--workers",
|
||||||
|
dest="workers",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Number of worker processes [default 1]",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--fast",
|
||||||
|
dest="fast",
|
||||||
|
action="store_true",
|
||||||
|
help="Set the number of workers to max allowed",
|
||||||
|
)
|
||||||
|
self.add_bool_arguments(
|
||||||
|
"--access-logs", dest="access_log", help="display access logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DevelopmentGroup(Group):
|
||||||
|
name = "Development"
|
||||||
|
|
||||||
|
def attach(self):
|
||||||
|
self.container.add_argument(
|
||||||
|
"--debug",
|
||||||
|
dest="debug",
|
||||||
|
action="store_true",
|
||||||
|
help="Run the server in debug mode",
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--dev",
|
||||||
|
dest="debug",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Currently is an alias for --debug. But starting in v22.3, \n"
|
||||||
|
"--debug will no longer automatically trigger auto_restart. \n"
|
||||||
|
"However, --dev will continue, effectively making it the \n"
|
||||||
|
"same as debug + auto_reload."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--reload",
|
||||||
|
"--auto-reload",
|
||||||
|
dest="auto_reload",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Watch source directory for file changes and reload on "
|
||||||
|
"changes"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"-R",
|
||||||
|
"--reload-dir",
|
||||||
|
dest="path",
|
||||||
|
action="append",
|
||||||
|
help="Extra directories to watch and reload on changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OutputGroup(Group):
|
||||||
|
name = "Output"
|
||||||
|
|
||||||
|
def attach(self):
|
||||||
|
self.add_bool_arguments(
|
||||||
|
"--motd",
|
||||||
|
dest="motd",
|
||||||
|
default=True,
|
||||||
|
help="Show the startup display",
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--verbosity",
|
||||||
|
action="count",
|
||||||
|
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
|
||||||
|
)
|
||||||
|
self.add_bool_arguments(
|
||||||
|
"--noisy-exceptions",
|
||||||
|
dest="noisy_exceptions",
|
||||||
|
help="Output stack traces for all exceptions",
|
||||||
|
)
|
@ -10,6 +10,13 @@ from multidict import CIMultiDict # type: ignore
|
|||||||
OS_IS_WINDOWS = os.name == "nt"
|
OS_IS_WINDOWS = os.name == "nt"
|
||||||
|
|
||||||
|
|
||||||
|
def enable_windows_color_support():
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
kernel = ctypes.windll.kernel32
|
||||||
|
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
|
||||||
|
|
||||||
|
|
||||||
class Header(CIMultiDict):
|
class Header(CIMultiDict):
|
||||||
"""
|
"""
|
||||||
Container used for both request and response headers. It is a subclass of
|
Container used for both request and response headers. It is a subclass of
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from inspect import isclass
|
from inspect import isclass
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional, Union
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.errorpages import check_error_format
|
from sanic.errorpages import check_error_format
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
|
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 = {
|
||||||
"ACCESS_LOG": True,
|
"ACCESS_LOG": True,
|
||||||
|
"AUTO_RELOAD": False,
|
||||||
"EVENT_AUTOREGISTER": False,
|
"EVENT_AUTOREGISTER": False,
|
||||||
"FALLBACK_ERROR_FORMAT": "auto",
|
"FALLBACK_ERROR_FORMAT": "auto",
|
||||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
@ -27,6 +28,9 @@ DEFAULT_CONFIG = {
|
|||||||
"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,
|
||||||
@ -44,6 +48,7 @@ DEFAULT_CONFIG = {
|
|||||||
|
|
||||||
class Config(dict):
|
class Config(dict):
|
||||||
ACCESS_LOG: bool
|
ACCESS_LOG: bool
|
||||||
|
AUTO_RELOAD: bool
|
||||||
EVENT_AUTOREGISTER: bool
|
EVENT_AUTOREGISTER: bool
|
||||||
FALLBACK_ERROR_FORMAT: str
|
FALLBACK_ERROR_FORMAT: str
|
||||||
FORWARDED_FOR_HEADER: str
|
FORWARDED_FOR_HEADER: str
|
||||||
@ -51,6 +56,9 @@ class Config(dict):
|
|||||||
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
|
||||||
@ -71,11 +79,14 @@ class Config(dict):
|
|||||||
load_env: Optional[Union[bool, str]] = True,
|
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,
|
||||||
|
*,
|
||||||
|
app: Optional[Sanic] = None,
|
||||||
):
|
):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
|
||||||
self.LOGO = BASE_LOGO
|
self._app = app
|
||||||
|
self._LOGO = ""
|
||||||
|
|
||||||
if keep_alive is not None:
|
if keep_alive is not None:
|
||||||
self.KEEP_ALIVE = keep_alive
|
self.KEEP_ALIVE = keep_alive
|
||||||
@ -97,6 +108,7 @@ class Config(dict):
|
|||||||
|
|
||||||
self._configure_header_size()
|
self._configure_header_size()
|
||||||
self._check_error_format()
|
self._check_error_format()
|
||||||
|
self._init = True
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
try:
|
try:
|
||||||
@ -104,8 +116,20 @@ class Config(dict):
|
|||||||
except KeyError as ke:
|
except KeyError as ke:
|
||||||
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value) -> None:
|
||||||
self[attr] = value
|
self.update({attr: value})
|
||||||
|
|
||||||
|
def __setitem__(self, attr, value) -> None:
|
||||||
|
self.update({attr: value})
|
||||||
|
|
||||||
|
def update(self, *other, **kwargs) -> None:
|
||||||
|
other_mapping = {k: v for item in other for k, v in dict(item).items()}
|
||||||
|
super().update(*other, **kwargs)
|
||||||
|
for attr, value in {**other_mapping, **kwargs}.items():
|
||||||
|
self._post_set(attr, value)
|
||||||
|
|
||||||
|
def _post_set(self, attr, value) -> None:
|
||||||
|
if self.get("_init"):
|
||||||
if attr in (
|
if attr in (
|
||||||
"REQUEST_MAX_HEADER_SIZE",
|
"REQUEST_MAX_HEADER_SIZE",
|
||||||
"REQUEST_BUFFER_SIZE",
|
"REQUEST_BUFFER_SIZE",
|
||||||
@ -114,6 +138,29 @@ class Config(dict):
|
|||||||
self._configure_header_size()
|
self._configure_header_size()
|
||||||
elif attr == "FALLBACK_ERROR_FORMAT":
|
elif attr == "FALLBACK_ERROR_FORMAT":
|
||||||
self._check_error_format()
|
self._check_error_format()
|
||||||
|
if self.app and value != self.app.error_handler.fallback:
|
||||||
|
if self.app.error_handler.fallback != "auto":
|
||||||
|
warn(
|
||||||
|
"Overriding non-default ErrorHandler fallback "
|
||||||
|
"value. Changing from "
|
||||||
|
f"{self.app.error_handler.fallback} to {value}."
|
||||||
|
)
|
||||||
|
self.app.error_handler.fallback = value
|
||||||
|
elif attr == "LOGO":
|
||||||
|
self._LOGO = value
|
||||||
|
warn(
|
||||||
|
"Setting the config.LOGO is deprecated and will no longer "
|
||||||
|
"be supported starting in v22.6.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app(self):
|
||||||
|
return self._app
|
||||||
|
|
||||||
|
@property
|
||||||
|
def LOGO(self):
|
||||||
|
return self._LOGO
|
||||||
|
|
||||||
def _configure_header_size(self):
|
def _configure_header_size(self):
|
||||||
Http.set_header_max_size(
|
Http.set_header_max_size(
|
||||||
@ -127,11 +174,11 @@ class Config(dict):
|
|||||||
|
|
||||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||||
"""
|
"""
|
||||||
Looks for prefixed environment variables and applies
|
Looks for prefixed environment variables and applies them to the
|
||||||
them to the configuration if present. This is called automatically when
|
configuration if present. This is called automatically when Sanic
|
||||||
Sanic starts up to load environment variables into config.
|
starts up to load environment variables into config.
|
||||||
|
|
||||||
It will automatically hyrdate the following types:
|
It will automatically hydrate the following types:
|
||||||
|
|
||||||
- ``int``
|
- ``int``
|
||||||
- ``float``
|
- ``float``
|
||||||
@ -139,19 +186,18 @@ class Config(dict):
|
|||||||
|
|
||||||
Anything else will be imported as a ``str``.
|
Anything else will be imported as a ``str``.
|
||||||
"""
|
"""
|
||||||
for k, v in environ.items():
|
for key, value in environ.items():
|
||||||
if k.startswith(prefix):
|
if not key.startswith(prefix):
|
||||||
_, config_key = k.split(prefix, 1)
|
continue
|
||||||
|
|
||||||
|
_, config_key = key.split(prefix, 1)
|
||||||
|
|
||||||
|
for converter in (int, float, str_to_bool, str):
|
||||||
try:
|
try:
|
||||||
self[config_key] = int(v)
|
self[config_key] = converter(value)
|
||||||
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
pass
|
||||||
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]):
|
||||||
"""
|
"""
|
||||||
|
@ -25,12 +25,13 @@ 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 # type: ignore
|
from json import dumps
|
||||||
|
|
||||||
|
|
||||||
FALLBACK_TEXT = (
|
FALLBACK_TEXT = (
|
||||||
@ -45,6 +46,8 @@ 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
|
||||||
@ -112,14 +115,16 @@ 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 { margin: 0 }
|
.tb-wrapper p, dl, dd { margin: 0 }
|
||||||
.frame-border { margin: 1rem }
|
.frame-border { margin: 1rem }
|
||||||
.frame-line > * { padding: 0.3rem 0.6rem }
|
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
|
||||||
.frame-line { margin-bottom: 0.3rem }
|
.frame-line, dl { margin-bottom: 0.3rem }
|
||||||
.frame-code { font-size: 16px; padding-left: 4ch }
|
.frame-code, dd { font-size: 16px; padding-left: 4ch }
|
||||||
.tb-wrapper { border: 1px solid #eee }
|
.tb-wrapper, dl { border: 1px solid #eee }
|
||||||
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
|
.tb-header,.obj-header {
|
||||||
.frame-descriptor { background: #e2eafb; font-size: 14px }
|
background: #eee; padding: 0.3rem; font-weight: bold
|
||||||
|
}
|
||||||
|
.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>"
|
||||||
@ -138,6 +143,11 @@ 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"
|
||||||
@ -152,7 +162,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(),
|
body=self._generate_body(full=True),
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
)
|
)
|
||||||
@ -163,7 +173,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="",
|
body=self._generate_body(full=False),
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
@ -177,7 +187,9 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
def title(self):
|
def title(self):
|
||||||
return escape(f"⚠️ {super().title}")
|
return escape(f"⚠️ {super().title}")
|
||||||
|
|
||||||
def _generate_body(self):
|
def _generate_body(self, *, full):
|
||||||
|
lines = []
|
||||||
|
if full:
|
||||||
_, exc_value, __ = sys.exc_info()
|
_, exc_value, __ = sys.exc_info()
|
||||||
exceptions = []
|
exceptions = []
|
||||||
while exc_value:
|
while exc_value:
|
||||||
@ -189,15 +201,35 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
name = escape(self.exception.__class__.__name__)
|
name = escape(self.exception.__class__.__name__)
|
||||||
value = escape(self.exception)
|
value = escape(self.exception)
|
||||||
path = escape(self.request.path)
|
path = escape(self.request.path)
|
||||||
lines = [
|
lines += [
|
||||||
f"<h2>Traceback of {appname} (most recent call last):</h2>",
|
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
|
||||||
f"{traceback_html}",
|
f"{traceback_html}",
|
||||||
"<div class=summary><p>",
|
"<div class=summary><p>",
|
||||||
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
|
f"<b>{name}: {value}</b> "
|
||||||
|
f"while handling path <code>{path}</code>",
|
||||||
"</div>",
|
"</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))
|
||||||
|
|
||||||
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(
|
||||||
@ -224,7 +256,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(),
|
body=self._generate_body(full=True),
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
)
|
)
|
||||||
@ -235,7 +267,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="",
|
body=self._generate_body(full=False),
|
||||||
),
|
),
|
||||||
status=self.status,
|
status=self.status,
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
@ -245,21 +277,31 @@ class TextRenderer(BaseRenderer):
|
|||||||
def title(self):
|
def title(self):
|
||||||
return f"⚠️ {super().title}"
|
return f"⚠️ {super().title}"
|
||||||
|
|
||||||
def _generate_body(self):
|
def _generate_body(self, *, full):
|
||||||
|
lines = []
|
||||||
|
if full:
|
||||||
_, exc_value, __ = sys.exc_info()
|
_, exc_value, __ = sys.exc_info()
|
||||||
exceptions = []
|
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} (most recent call last):\n",
|
f"Traceback of {self.request.app.name} "
|
||||||
|
"(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__
|
||||||
|
|
||||||
return "\n".join(lines + exceptions[::-1])
|
lines += exceptions[::-1]
|
||||||
|
|
||||||
|
for attr, display in (("context", True), ("extra", bool(full))):
|
||||||
|
info = getattr(self.exception, attr, None)
|
||||||
|
if info and display:
|
||||||
|
lines += self._generate_object_display_list(info, attr)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _format_exc(self, exc):
|
def _format_exc(self, exc):
|
||||||
frames = "\n\n".join(
|
frames = "\n\n".join(
|
||||||
@ -272,6 +314,13 @@ 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):
|
||||||
"""
|
"""
|
||||||
@ -280,11 +329,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=dumps)
|
return json(output, status=self.status, dumps=self.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=dumps)
|
return json(output, status=self.status, dumps=self.dumps)
|
||||||
|
|
||||||
def _generate_output(self, *, full):
|
def _generate_output(self, *, full):
|
||||||
output = {
|
output = {
|
||||||
@ -293,6 +342,11 @@ 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 = []
|
||||||
@ -393,6 +447,7 @@ def exception_response(
|
|||||||
# from the route
|
# from the route
|
||||||
if request.route:
|
if request.route:
|
||||||
try:
|
try:
|
||||||
|
if request.route.ctx.error_format:
|
||||||
render_format = request.route.ctx.error_format
|
render_format = request.route.ctx.error_format
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
...
|
...
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional, Union
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
@ -11,7 +11,11 @@ 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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from inspect import signature
|
from inspect import signature
|
||||||
from typing import Dict, List, Optional, Tuple, Type
|
from typing import Dict, List, Optional, Tuple, Type
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
|
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
@ -38,7 +39,14 @@ class ErrorHandler:
|
|||||||
self.base = base
|
self.base = base
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def finalize(cls, error_handler):
|
def finalize(cls, error_handler, fallback: Optional[str] = None):
|
||||||
|
if (
|
||||||
|
fallback
|
||||||
|
and fallback != "auto"
|
||||||
|
and error_handler.fallback == "auto"
|
||||||
|
):
|
||||||
|
error_handler.fallback = fallback
|
||||||
|
|
||||||
if not isinstance(error_handler, cls):
|
if not isinstance(error_handler, cls):
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
f"Error handler is non-conforming: {type(error_handler)}"
|
f"Error handler is non-conforming: {type(error_handler)}"
|
||||||
@ -46,16 +54,15 @@ class ErrorHandler:
|
|||||||
|
|
||||||
sig = signature(error_handler.lookup)
|
sig = signature(error_handler.lookup)
|
||||||
if len(sig.parameters) == 1:
|
if len(sig.parameters) == 1:
|
||||||
error_logger.warning(
|
warn(
|
||||||
DeprecationWarning(
|
|
||||||
"You are using a deprecated error handler. The lookup "
|
"You are using a deprecated error handler. The lookup "
|
||||||
"method should accept two positional parameters: "
|
"method should accept two positional parameters: "
|
||||||
"(exception, route_name: Optional[str]). "
|
"(exception, route_name: Optional[str]). "
|
||||||
"Until you upgrade your ErrorHandler.lookup, Blueprint "
|
"Until you upgrade your ErrorHandler.lookup, Blueprint "
|
||||||
"specific exceptions will not work properly. Beginning "
|
"specific exceptions will not work properly. Beginning "
|
||||||
"in v22.3, the legacy style lookup method will not "
|
"in v22.3, the legacy style lookup method will not "
|
||||||
"work at all."
|
"work at all.",
|
||||||
),
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
error_handler._lookup = error_handler._legacy_lookup
|
error_handler._lookup = error_handler._legacy_lookup
|
||||||
|
|
||||||
@ -192,7 +199,8 @@ class ErrorHandler:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def log(request, exception):
|
def log(request, exception):
|
||||||
quiet = getattr(exception, "quiet", False)
|
quiet = getattr(exception, "quiet", False)
|
||||||
if quiet is False:
|
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
|
||||||
|
if quiet is False or noisy is True:
|
||||||
try:
|
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 espaces in a way that would allow perfect handling.
|
# even though no client escapes in a way that would allow perfect handling.
|
||||||
|
|
||||||
# For more information, consult ../tests/test_requests.py
|
# 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
|
||||||
instanciate a class
|
instantiate 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
|
||||||
|
|
||||||
|
@ -105,7 +105,6 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
self.keep_alive = True
|
self.keep_alive = True
|
||||||
self.stage: Stage = Stage.IDLE
|
self.stage: Stage = Stage.IDLE
|
||||||
self.dispatch = self.protocol.app.dispatch
|
self.dispatch = self.protocol.app.dispatch
|
||||||
self.init_for_request()
|
|
||||||
|
|
||||||
def init_for_request(self):
|
def init_for_request(self):
|
||||||
"""Init/reset all per-request variables."""
|
"""Init/reset all per-request variables."""
|
||||||
@ -129,14 +128,20 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
HTTP 1.1 connection handler
|
HTTP 1.1 connection handler
|
||||||
"""
|
"""
|
||||||
while True: # As long as connection stays keep-alive
|
# Handle requests while the connection stays reusable
|
||||||
|
while self.keep_alive and self.stage is Stage.IDLE:
|
||||||
|
self.init_for_request()
|
||||||
|
# Wait for incoming bytes (in IDLE stage)
|
||||||
|
if not self.recv_buffer:
|
||||||
|
await self._receive_more()
|
||||||
|
self.stage = Stage.REQUEST
|
||||||
try:
|
try:
|
||||||
# Receive and handle a request
|
# Receive and handle a request
|
||||||
self.stage = Stage.REQUEST
|
|
||||||
self.response_func = self.http1_response_header
|
self.response_func = self.http1_response_header
|
||||||
|
|
||||||
await self.http1_request_header()
|
await self.http1_request_header()
|
||||||
|
|
||||||
|
self.stage = Stage.HANDLER
|
||||||
self.request.conn_info = self.protocol.conn_info
|
self.request.conn_info = self.protocol.conn_info
|
||||||
await self.protocol.request_handler(self.request)
|
await self.protocol.request_handler(self.request)
|
||||||
|
|
||||||
@ -187,16 +192,6 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
if self.response:
|
if self.response:
|
||||||
self.response.stream = None
|
self.response.stream = None
|
||||||
|
|
||||||
# Exit and disconnect if no more requests can be taken
|
|
||||||
if self.stage is not Stage.IDLE or not self.keep_alive:
|
|
||||||
break
|
|
||||||
|
|
||||||
self.init_for_request()
|
|
||||||
|
|
||||||
# Wait for the next request
|
|
||||||
if not self.recv_buffer:
|
|
||||||
await self._receive_more()
|
|
||||||
|
|
||||||
async def http1_request_header(self): # no cov
|
async def http1_request_header(self): # no cov
|
||||||
"""
|
"""
|
||||||
Receive and parse request header into self.request.
|
Receive and parse request header into self.request.
|
||||||
@ -299,7 +294,6 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
# Remove header and its trailing CRLF
|
# Remove header and its trailing CRLF
|
||||||
del buf[: pos + 4]
|
del buf[: pos + 4]
|
||||||
self.stage = Stage.HANDLER
|
|
||||||
self.request, request.stream = request, self
|
self.request, request.stream = request, self
|
||||||
self.protocol.state["requests_count"] += 1
|
self.protocol.state["requests_count"] += 1
|
||||||
|
|
||||||
@ -590,6 +584,11 @@ 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
|
||||||
|
|
||||||
|
13
sanic/log.py
13
sanic/log.py
@ -1,8 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
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={
|
||||||
@ -53,6 +56,14 @@ LOGGING_CONFIG_DEFAULTS = dict(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Colors(str, Enum):
|
||||||
|
END = "\033[0m"
|
||||||
|
BLUE = "\033[01;34m"
|
||||||
|
GREEN = "\033[01;32m"
|
||||||
|
YELLOW = "\033[01;33m"
|
||||||
|
RED = "\033[01;31m"
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("sanic.root")
|
logger = logging.getLogger("sanic.root")
|
||||||
"""
|
"""
|
||||||
General Sanic logger
|
General Sanic logger
|
||||||
|
@ -3,7 +3,7 @@ from functools import partial
|
|||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from sanic.models.futures import FutureListener
|
from sanic.models.futures import FutureListener
|
||||||
from sanic.models.handler_types import ListenerType
|
from sanic.models.handler_types import ListenerType, Sanic
|
||||||
|
|
||||||
|
|
||||||
class ListenerEvent(str, Enum):
|
class ListenerEvent(str, Enum):
|
||||||
@ -27,10 +27,10 @@ class ListenerMixin:
|
|||||||
|
|
||||||
def listener(
|
def listener(
|
||||||
self,
|
self,
|
||||||
listener_or_event: Union[ListenerType, str],
|
listener_or_event: Union[ListenerType[Sanic], 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.
|
||||||
|
|
||||||
@ -62,20 +62,32 @@ class ListenerMixin:
|
|||||||
else:
|
else:
|
||||||
return partial(register_listener, event=listener_or_event)
|
return partial(register_listener, event=listener_or_event)
|
||||||
|
|
||||||
def main_process_start(self, listener: ListenerType) -> ListenerType:
|
def main_process_start(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "main_process_start")
|
return self.listener(listener, "main_process_start")
|
||||||
|
|
||||||
def main_process_stop(self, listener: ListenerType) -> ListenerType:
|
def main_process_stop(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "main_process_stop")
|
return self.listener(listener, "main_process_stop")
|
||||||
|
|
||||||
def before_server_start(self, listener: ListenerType) -> ListenerType:
|
def before_server_start(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "before_server_start")
|
return self.listener(listener, "before_server_start")
|
||||||
|
|
||||||
def after_server_start(self, listener: ListenerType) -> ListenerType:
|
def after_server_start(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "after_server_start")
|
return self.listener(listener, "after_server_start")
|
||||||
|
|
||||||
def before_server_stop(self, listener: ListenerType) -> ListenerType:
|
def before_server_stop(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "before_server_stop")
|
return self.listener(listener, "before_server_stop")
|
||||||
|
|
||||||
def after_server_stop(self, listener: ListenerType) -> ListenerType:
|
def after_server_stop(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "after_server_stop")
|
return self.listener(listener, "after_server_stop")
|
||||||
|
@ -52,7 +52,7 @@ class RouteMixin:
|
|||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
methods: Optional[Iterable[str]] = None,
|
methods: Optional[Iterable[str]] = None,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[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,
|
||||||
@ -189,9 +189,9 @@ class RouteMixin:
|
|||||||
handler: RouteHandler,
|
handler: RouteHandler,
|
||||||
uri: str,
|
uri: str,
|
||||||
methods: Iterable[str] = frozenset({"GET"}),
|
methods: Iterable[str] = frozenset({"GET"}),
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
@ -254,9 +254,9 @@ class RouteMixin:
|
|||||||
def get(
|
def get(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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",
|
||||||
@ -290,10 +290,10 @@ class RouteMixin:
|
|||||||
def post(
|
def post(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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,
|
||||||
@ -326,10 +326,10 @@ class RouteMixin:
|
|||||||
def put(
|
def put(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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,
|
||||||
@ -362,9 +362,9 @@ class RouteMixin:
|
|||||||
def head(
|
def head(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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",
|
||||||
@ -406,9 +406,9 @@ class RouteMixin:
|
|||||||
def options(
|
def options(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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",
|
||||||
@ -450,10 +450,10 @@ class RouteMixin:
|
|||||||
def patch(
|
def patch(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
stream=False,
|
stream=False,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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,
|
||||||
@ -496,9 +496,9 @@ class RouteMixin:
|
|||||||
def delete(
|
def delete(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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",
|
||||||
@ -532,10 +532,10 @@ class RouteMixin:
|
|||||||
def websocket(
|
def websocket(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
subprotocols: Optional[List[str]] = None,
|
subprotocols: Optional[List[str]] = None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
@ -573,10 +573,10 @@ class RouteMixin:
|
|||||||
self,
|
self,
|
||||||
handler,
|
handler,
|
||||||
uri: str,
|
uri: str,
|
||||||
host: Optional[str] = None,
|
host: Optional[Union[str, List[str]]] = None,
|
||||||
strict_slashes: Optional[bool] = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
subprotocols=None,
|
subprotocols=None,
|
||||||
version: Optional[int] = None,
|
version: Optional[Union[int, str, float]] = 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,
|
||||||
@ -918,7 +918,7 @@ class RouteMixin:
|
|||||||
|
|
||||||
return route
|
return route
|
||||||
|
|
||||||
def _determine_error_format(self, handler) -> str:
|
def _determine_error_format(self, handler) -> Optional[str]:
|
||||||
if not isinstance(handler, CompositionView):
|
if not isinstance(handler, CompositionView):
|
||||||
try:
|
try:
|
||||||
src = dedent(getsource(handler))
|
src = dedent(getsource(handler))
|
||||||
@ -930,7 +930,7 @@ class RouteMixin:
|
|||||||
except (OSError, TypeError):
|
except (OSError, TypeError):
|
||||||
...
|
...
|
||||||
|
|
||||||
return "auto"
|
return None
|
||||||
|
|
||||||
def _get_response_types(self, node):
|
def _get_response_types(self, node):
|
||||||
types = set()
|
types = set()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from typing import Any, Callable, Dict, Optional, Set
|
from enum import Enum
|
||||||
|
from typing import Any, Callable, Dict, Optional, Set, Union
|
||||||
|
|
||||||
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
|
||||||
@ -19,7 +20,7 @@ class SignalMixin:
|
|||||||
|
|
||||||
def signal(
|
def signal(
|
||||||
self,
|
self,
|
||||||
event: str,
|
event: Union[str, Enum],
|
||||||
*,
|
*,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Dict[str, Any] = None,
|
||||||
@ -41,13 +42,11 @@ class SignalMixin:
|
|||||||
filtering, defaults to None
|
filtering, defaults to None
|
||||||
: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, HashableDict(condition or {})
|
handler, event_value, HashableDict(condition or {})
|
||||||
)
|
)
|
||||||
self._future_signals.add(future_signal)
|
self._future_signals.add(future_signal)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
||||||
|
|
||||||
@ -14,7 +15,17 @@ 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
|
||||||
|
loop = loop if sys.version_info[:2] < (3, 8) else None
|
||||||
|
# Optional in 3.9, necessary in 3.10 because the parameter "loop"
|
||||||
|
# was completely removed
|
||||||
|
if not loop:
|
||||||
|
self._not_paused = asyncio.Event()
|
||||||
|
self._not_paused.set()
|
||||||
|
self._complete = asyncio.Event()
|
||||||
|
else:
|
||||||
self._not_paused = asyncio.Event(loop=loop)
|
self._not_paused = asyncio.Event(loop=loop)
|
||||||
self._not_paused.set()
|
self._not_paused.set()
|
||||||
self._complete = asyncio.Event(loop=loop)
|
self._complete = asyncio.Event(loop=loop)
|
||||||
|
@ -13,7 +13,7 @@ class FutureRoute(NamedTuple):
|
|||||||
handler: str
|
handler: str
|
||||||
uri: str
|
uri: str
|
||||||
methods: Optional[Iterable[str]]
|
methods: Optional[Iterable[str]]
|
||||||
host: str
|
host: Union[str, List[str]]
|
||||||
strict_slashes: bool
|
strict_slashes: bool
|
||||||
stream: bool
|
stream: bool
|
||||||
version: Optional[int]
|
version: Optional[int]
|
||||||
@ -60,3 +60,7 @@ class FutureSignal(NamedTuple):
|
|||||||
handler: SignalHandler
|
handler: SignalHandler
|
||||||
event: str
|
event: str
|
||||||
condition: Optional[Dict[str, str]]
|
condition: Optional[Dict[str, str]]
|
||||||
|
|
||||||
|
|
||||||
|
class FutureRegistry(set):
|
||||||
|
...
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
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
|
||||||
|
|
||||||
@ -20,8 +22,10 @@ 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):
|
||||||
@ -31,8 +35,16 @@ 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: bool = bool(transport.get_extra_info("sslcontext"))
|
self.ssl = False
|
||||||
|
self.server_name = ""
|
||||||
|
self.cert: Dict[str, Any] = {}
|
||||||
|
sslobj: Optional[SSLObject] = transport.get_extra_info(
|
||||||
|
"ssl_object"
|
||||||
|
) # type: ignore
|
||||||
|
if sslobj:
|
||||||
|
self.ssl = True
|
||||||
|
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
|
||||||
|
self.cert = dict(getattr(sslobj.context, "sanic", {}))
|
||||||
if isinstance(addr, str): # UNIX socket
|
if isinstance(addr, str): # UNIX socket
|
||||||
self.server = unix or addr
|
self.server = unix or addr
|
||||||
return
|
return
|
||||||
|
@ -6,9 +6,6 @@ import sys
|
|||||||
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from sanic.config import BASE_LOGO
|
|
||||||
from sanic.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
def _iter_module_files():
|
def _iter_module_files():
|
||||||
"""This iterates over all relevant Python files.
|
"""This iterates over all relevant Python files.
|
||||||
@ -50,13 +47,19 @@ def _get_args_for_reloading():
|
|||||||
return [sys.executable] + sys.argv
|
return [sys.executable] + sys.argv
|
||||||
|
|
||||||
|
|
||||||
def restart_with_reloader():
|
def restart_with_reloader(changed=None):
|
||||||
"""Create a new process and a subprocess in it with the same arguments as
|
"""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={**os.environ, "SANIC_SERVER_RUNNING": "true"},
|
env={
|
||||||
|
**os.environ,
|
||||||
|
"SANIC_SERVER_RUNNING": "true",
|
||||||
|
"SANIC_RELOADER_PROCESS": "true",
|
||||||
|
"SANIC_RELOADED_FILES": reloaded,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -91,31 +94,29 @@ 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:
|
||||||
check = _check_file(filename, mtimes)
|
if _check_file(filename, mtimes):
|
||||||
|
path = (
|
||||||
|
filename
|
||||||
|
if isinstance(filename, str)
|
||||||
|
else filename.resolve()
|
||||||
|
)
|
||||||
|
changed.add(str(path))
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if check:
|
if changed:
|
||||||
need_reload = True
|
|
||||||
|
|
||||||
if need_reload:
|
|
||||||
worker_process.terminate()
|
worker_process.terminate()
|
||||||
worker_process.wait()
|
worker_process.wait()
|
||||||
worker_process = restart_with_reloader()
|
worker_process = restart_with_reloader(changed)
|
||||||
|
|
||||||
sleep(sleep_interval)
|
sleep(sleep_interval)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
@ -18,7 +18,6 @@ from sanic_routing.route import Route # type: ignore
|
|||||||
if TYPE_CHECKING:
|
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
|
||||||
@ -32,7 +31,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
|
from sanic.exceptions import InvalidUsage, ServerError
|
||||||
from sanic.headers import (
|
from sanic.headers import (
|
||||||
AcceptContainer,
|
AcceptContainer,
|
||||||
Options,
|
Options,
|
||||||
@ -42,6 +41,7 @@ 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,6 +104,7 @@ 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",
|
||||||
@ -155,6 +156,7 @@ 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__
|
||||||
@ -164,6 +166,21 @@ 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,
|
||||||
@ -172,13 +189,19 @@ 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 = (self.stream and self.stream.response) or HTTPResponse(
|
response = 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)
|
||||||
@ -193,6 +216,7 @@ 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):
|
||||||
@ -760,9 +784,10 @@ 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[colon_index + 2 :]
|
form_line[idx:]
|
||||||
)
|
)
|
||||||
|
|
||||||
if form_header_field == "content-disposition":
|
if form_header_field == "content-disposition":
|
||||||
|
@ -3,6 +3,7 @@ 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,
|
||||||
@ -19,11 +20,15 @@ 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
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ujson import dumps as json_dumps
|
from ujson import dumps as json_dumps
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -45,7 +50,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: Http = None
|
self.stream: Optional[Union[Http, ASGIApp]] = 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
|
||||||
@ -101,7 +106,7 @@ class BaseHTTPResponse:
|
|||||||
|
|
||||||
async def send(
|
async def send(
|
||||||
self,
|
self,
|
||||||
data: Optional[Union[AnyStr]] = None,
|
data: Optional[AnyStr] = None,
|
||||||
end_stream: Optional[bool] = None,
|
end_stream: Optional[bool] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -112,8 +117,17 @@ 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 end_stream and not data and self.stream.send is None:
|
if self.stream is None:
|
||||||
|
raise SanicException(
|
||||||
|
"No stream is connected to the response object instance."
|
||||||
|
)
|
||||||
|
if self.stream.send is None:
|
||||||
|
if end_stream and not data:
|
||||||
return
|
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")
|
||||||
|
@ -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 containg the details about how to handle
|
Retrieve a `Route` object containing 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
|
||||||
@ -139,10 +139,9 @@ class Router(BaseRouter):
|
|||||||
route.ctx.stream = stream
|
route.ctx.stream = stream
|
||||||
route.ctx.hosts = hosts
|
route.ctx.hosts = hosts
|
||||||
route.ctx.static = static
|
route.ctx.static = static
|
||||||
route.ctx.error_format = (
|
route.ctx.error_format = error_format
|
||||||
error_format or self.ctx.app.config.FALLBACK_ERROR_FORMAT
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if error_format:
|
||||||
check_error_format(route.ctx.error_format)
|
check_error_format(route.ctx.error_format)
|
||||||
|
|
||||||
routes.append(route)
|
routes.append(route)
|
||||||
|
@ -2,20 +2,27 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
class AsyncioServer:
|
class AsyncioServer:
|
||||||
"""
|
"""
|
||||||
Wraps an asyncio server with functionality that might be useful to
|
Wraps an asyncio server with functionality that might be useful to
|
||||||
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", "init")
|
__slots__ = ("app", "connections", "loop", "serve_coro", "server")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
app,
|
app: Sanic,
|
||||||
loop,
|
loop,
|
||||||
serve_coro,
|
serve_coro,
|
||||||
connections,
|
connections,
|
||||||
@ -27,13 +34,20 @@ 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):
|
||||||
|
warn(
|
||||||
|
"AsyncioServer.init has been deprecated and will be removed "
|
||||||
|
"in v22.6. Use Sanic.state.is_started instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
return self.app.state.is_started
|
||||||
|
|
||||||
def startup(self):
|
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):
|
||||||
@ -77,30 +91,33 @@ class AsyncioServer:
|
|||||||
return task
|
return task
|
||||||
|
|
||||||
def start_serving(self):
|
def start_serving(self):
|
||||||
if self.server:
|
return self._serve(self.server.start_serving)
|
||||||
try:
|
|
||||||
return self.server.start_serving()
|
|
||||||
except AttributeError:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"server.start_serving not available in this version "
|
|
||||||
"of asyncio or uvloop."
|
|
||||||
)
|
|
||||||
|
|
||||||
def serve_forever(self):
|
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:
|
||||||
|
raise SanicException(
|
||||||
|
"Cannot run Sanic server without first running "
|
||||||
|
"await server.startup()"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.server.serve_forever()
|
return serve_func()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
name = serve_func.__name__
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"server.serve_forever not available in this version "
|
f"server.{name} 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.init:
|
if not self.app.state.is_started:
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
"Cannot dispatch server event without "
|
"Cannot dispatch server event without "
|
||||||
"first running server.startup()"
|
"first running await server.startup()"
|
||||||
)
|
)
|
||||||
return self.app._server_event(concern, action, loop=self.loop)
|
return self.app._server_event(concern, action, loop=self.loop)
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from typing import TYPE_CHECKING, Optional, Sequence
|
from typing import TYPE_CHECKING, Optional, Sequence, cast
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
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 error_logger
|
from sanic.log import error_logger
|
||||||
@ -15,13 +17,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class WebSocketProtocol(HttpProtocol):
|
class WebSocketProtocol(HttpProtocol):
|
||||||
|
|
||||||
websocket: Optional[WebsocketImplProtocol]
|
|
||||||
websocket_timeout: float
|
|
||||||
websocket_max_size = Optional[int]
|
|
||||||
websocket_ping_interval = Optional[float]
|
|
||||||
websocket_ping_timeout = Optional[float]
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args,
|
*args,
|
||||||
@ -35,32 +30,29 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.websocket = None
|
self.websocket: Optional[WebsocketImplProtocol] = 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
|
||||||
error_logger.warning(
|
warn(
|
||||||
DeprecationWarning(
|
|
||||||
"Websocket no longer uses queueing, so websocket_max_queue"
|
"Websocket no longer uses queueing, so websocket_max_queue"
|
||||||
" is no longer required."
|
" is no longer required.",
|
||||||
)
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
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
|
||||||
error_logger.warning(
|
warn(
|
||||||
DeprecationWarning(
|
|
||||||
"Websocket no longer uses read buffers, so "
|
"Websocket no longer uses read buffers, so "
|
||||||
"websocket_read_limit is not required."
|
"websocket_read_limit is not required.",
|
||||||
)
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
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
|
||||||
error_logger.warning(
|
warn(
|
||||||
DeprecationWarning(
|
|
||||||
"Websocket no longer uses write buffers, so "
|
"Websocket no longer uses write buffers, so "
|
||||||
"websocket_write_limit is not required."
|
"websocket_write_limit is not required.",
|
||||||
)
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
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
|
||||||
@ -109,14 +101,22 @@ 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]]
|
self, request, subprotocols: Optional[Sequence[str]] = None
|
||||||
):
|
):
|
||||||
# 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 = list(subprotocols)
|
subprotocols = cast(
|
||||||
|
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,
|
||||||
@ -131,21 +131,18 @@ 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:
|
||||||
rbody = "".join(
|
first_line = (
|
||||||
[
|
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
|
||||||
"HTTP/1.1 ",
|
).encode()
|
||||||
str(resp.status_code),
|
rbody = bytearray(first_line)
|
||||||
" ",
|
rbody += (
|
||||||
resp.reason_phrase,
|
"".join([f"{k}: {v}\r\n" for k, v in resp.headers.items()])
|
||||||
"\r\n",
|
).encode()
|
||||||
]
|
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 += f"\r\n{resp.body}\r\n\r\n"
|
rbody += resp.body
|
||||||
else:
|
rbody += b"\r\n\r\n"
|
||||||
rbody += "\r\n"
|
await super().send(rbody)
|
||||||
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(
|
||||||
|
@ -134,6 +134,7 @@ 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:
|
||||||
@ -181,7 +182,6 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@ -249,7 +249,10 @@ 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(target=serve, kwargs=server_settings)
|
process = mp.Process(
|
||||||
|
target=serve,
|
||||||
|
kwargs=server_settings,
|
||||||
|
)
|
||||||
process.daemon = True
|
process.daemon = True
|
||||||
process.start()
|
process.start()
|
||||||
processes.append(process)
|
processes.append(process)
|
||||||
|
@ -2,6 +2,7 @@ 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
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
@ -14,29 +15,47 @@ 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": (
|
||||||
# "server.main.start",
|
Event.SERVER_INIT_AFTER.value,
|
||||||
# "server.main.stop",
|
Event.SERVER_INIT_BEFORE.value,
|
||||||
"server.init.before",
|
Event.SERVER_SHUTDOWN_AFTER.value,
|
||||||
"server.init.after",
|
Event.SERVER_SHUTDOWN_BEFORE.value,
|
||||||
"server.shutdown.before",
|
|
||||||
"server.shutdown.after",
|
|
||||||
),
|
),
|
||||||
"http": (
|
"http": (
|
||||||
"http.lifecycle.begin",
|
Event.HTTP_LIFECYCLE_BEGIN.value,
|
||||||
"http.lifecycle.complete",
|
Event.HTTP_LIFECYCLE_COMPLETE.value,
|
||||||
"http.lifecycle.exception",
|
Event.HTTP_LIFECYCLE_EXCEPTION.value,
|
||||||
"http.lifecycle.handle",
|
Event.HTTP_LIFECYCLE_HANDLE.value,
|
||||||
"http.lifecycle.read_body",
|
Event.HTTP_LIFECYCLE_READ_BODY.value,
|
||||||
"http.lifecycle.read_head",
|
Event.HTTP_LIFECYCLE_READ_HEAD.value,
|
||||||
"http.lifecycle.request",
|
Event.HTTP_LIFECYCLE_REQUEST.value,
|
||||||
"http.lifecycle.response",
|
Event.HTTP_LIFECYCLE_RESPONSE.value,
|
||||||
"http.routing.after",
|
Event.HTTP_ROUTING_AFTER.value,
|
||||||
"http.routing.before",
|
Event.HTTP_ROUTING_BEFORE.value,
|
||||||
"http.lifecycle.send",
|
Event.HTTP_LIFECYCLE_SEND.value,
|
||||||
"http.middleware.after",
|
Event.HTTP_MIDDLEWARE_AFTER.value,
|
||||||
"http.middleware.before",
|
Event.HTTP_MIDDLEWARE_BEFORE.value,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +132,7 @@ class SignalRouter(BaseRouter):
|
|||||||
if fail_not_found:
|
if fail_not_found:
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
if self.ctx.app.debug:
|
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
|
||||||
error_logger.warning(str(e))
|
error_logger.warning(str(e))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
196
sanic/tls.py
Normal file
196
sanic/tls.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
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
|
@ -22,7 +22,9 @@ 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(self._registered_events).visit(tree)
|
node = RemoveDispatch(
|
||||||
|
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
|
||||||
@ -31,8 +33,9 @@ class OptionalDispatchEvent(BaseScheme):
|
|||||||
|
|
||||||
|
|
||||||
class RemoveDispatch(NodeTransformer):
|
class RemoveDispatch(NodeTransformer):
|
||||||
def __init__(self, registered_events) -> None:
|
def __init__(self, registered_events, verbosity: int = 0) -> 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
|
||||||
@ -49,6 +52,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
|
||||||
|
@ -48,7 +48,7 @@ def load_module_from_file_location(
|
|||||||
"""Returns loaded module provided as a file path.
|
"""Returns loaded module provided as a file path.
|
||||||
|
|
||||||
:param args:
|
:param args:
|
||||||
Coresponds to importlib.util.spec_from_file_location location
|
Corresponds to importlib.util.spec_from_file_location location
|
||||||
parameters,but with this differences:
|
parameters,but with this differences:
|
||||||
- It has to be of a string or bytes type.
|
- It has to be of a string or bytes type.
|
||||||
- You can also use here environment variables
|
- You can also use here environment variables
|
||||||
@ -58,10 +58,10 @@ def load_module_from_file_location(
|
|||||||
If location parameter is of a bytes type, then use this encoding
|
If location parameter is of a bytes type, then use this encoding
|
||||||
to decode it into string.
|
to decode it into string.
|
||||||
:param args:
|
:param args:
|
||||||
Coresponds to the rest of importlib.util.spec_from_file_location
|
Corresponds to the rest of importlib.util.spec_from_file_location
|
||||||
parameters.
|
parameters.
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
Coresponds to the rest of importlib.util.spec_from_file_location
|
Corresponds to the rest of importlib.util.spec_from_file_location
|
||||||
parameters.
|
parameters.
|
||||||
|
|
||||||
For example You can:
|
For example You can:
|
||||||
|
@ -310,7 +310,7 @@ if __name__ == "__main__":
|
|||||||
cli.add_argument(
|
cli.add_argument(
|
||||||
"--milestone",
|
"--milestone",
|
||||||
"-ms",
|
"-ms",
|
||||||
help="Git Release milestone information to include in relase note",
|
help="Git Release milestone information to include in release note",
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
cli.add_argument(
|
cli.add_argument(
|
||||||
|
7
setup.py
7
setup.py
@ -72,6 +72,7 @@ setup_kwargs = {
|
|||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
],
|
],
|
||||||
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
|
"entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]},
|
||||||
}
|
}
|
||||||
@ -94,7 +95,7 @@ requirements = [
|
|||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing>=0.7.0",
|
"sanic-testing>=0.7.0",
|
||||||
"pytest==5.2.1",
|
"pytest==6.2.5",
|
||||||
"coverage==5.3",
|
"coverage==5.3",
|
||||||
"gunicorn==20.0.4",
|
"gunicorn==20.0.4",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
@ -107,7 +108,7 @@ tests_require = [
|
|||||||
"black",
|
"black",
|
||||||
"isort>=5.0.0",
|
"isort>=5.0.0",
|
||||||
"bandit",
|
"bandit",
|
||||||
"mypy>=0.901",
|
"mypy>=0.901,<0.910",
|
||||||
"docutils",
|
"docutils",
|
||||||
"pygments",
|
"pygments",
|
||||||
"uvicorn<0.15.0",
|
"uvicorn<0.15.0",
|
||||||
@ -120,9 +121,11 @@ docs_require = [
|
|||||||
"docutils",
|
"docutils",
|
||||||
"pygments",
|
"pygments",
|
||||||
"m2r2",
|
"m2r2",
|
||||||
|
"mistune<2.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
dev_require = tests_require + [
|
dev_require = tests_require + [
|
||||||
|
"cryptography",
|
||||||
"tox",
|
"tox",
|
||||||
"towncrier",
|
"towncrier",
|
||||||
]
|
]
|
||||||
|
113
tests/certs/createcerts.py
Normal file
113
tests/certs/createcerts.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from ipaddress import ip_address
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||||
|
from cryptography.x509 import (
|
||||||
|
BasicConstraints,
|
||||||
|
CertificateBuilder,
|
||||||
|
DNSName,
|
||||||
|
ExtendedKeyUsage,
|
||||||
|
IPAddress,
|
||||||
|
KeyUsage,
|
||||||
|
Name,
|
||||||
|
NameAttribute,
|
||||||
|
SubjectAlternativeName,
|
||||||
|
random_serial_number,
|
||||||
|
)
|
||||||
|
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
||||||
|
|
||||||
|
|
||||||
|
def writefiles(key, cert):
|
||||||
|
cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
||||||
|
folder = path.join(path.dirname(__file__), cn)
|
||||||
|
with open(path.join(folder, "fullchain.pem"), "wb") as f:
|
||||||
|
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||||
|
|
||||||
|
with open(path.join(folder, "privkey.pem"), "wb") as f:
|
||||||
|
f.write(
|
||||||
|
key.private_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def selfsigned(key, common_name, san):
|
||||||
|
subject = issuer = Name(
|
||||||
|
[
|
||||||
|
NameAttribute(NameOID.COMMON_NAME, common_name),
|
||||||
|
NameAttribute(NameOID.ORGANIZATION_NAME, "Sanic Org"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
cert = (
|
||||||
|
CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(issuer)
|
||||||
|
.public_key(key.public_key())
|
||||||
|
.serial_number(random_serial_number())
|
||||||
|
.not_valid_before(datetime.utcnow())
|
||||||
|
.not_valid_after(datetime.utcnow() + timedelta(days=365.25 * 8))
|
||||||
|
.add_extension(
|
||||||
|
KeyUsage(
|
||||||
|
True, False, False, False, False, False, False, False, False
|
||||||
|
),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
ExtendedKeyUsage(
|
||||||
|
[
|
||||||
|
ExtendedKeyUsageOID.SERVER_AUTH,
|
||||||
|
ExtendedKeyUsageOID.CLIENT_AUTH,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
BasicConstraints(ca=True, path_length=None),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
SubjectAlternativeName(
|
||||||
|
[
|
||||||
|
IPAddress(ip_address(n))
|
||||||
|
if n[0].isdigit() or ":" in n
|
||||||
|
else DNSName(n)
|
||||||
|
for n in san
|
||||||
|
]
|
||||||
|
),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.sign(key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
return cert
|
||||||
|
|
||||||
|
|
||||||
|
# Sanic example/test self-signed cert RSA
|
||||||
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
cert = selfsigned(
|
||||||
|
key,
|
||||||
|
"sanic.example",
|
||||||
|
[
|
||||||
|
"sanic.example",
|
||||||
|
"www.sanic.example",
|
||||||
|
"*.sanic.test",
|
||||||
|
"2001:db8::541c",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
writefiles(key, cert)
|
||||||
|
|
||||||
|
# Sanic localhost self-signed cert ECDSA
|
||||||
|
key = ec.generate_private_key(ec.SECP256R1)
|
||||||
|
cert = selfsigned(
|
||||||
|
key,
|
||||||
|
"localhost",
|
||||||
|
[
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1",
|
||||||
|
"::1",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
writefiles(key, cert)
|
5
tests/certs/invalid.certmissing/privkey.pem
Normal file
5
tests/certs/invalid.certmissing/privkey.pem
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIFP3fCUob41U1wvVOvei4dGsXrZeSiBUCX/xVu9215bvoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEvBHo/RatEnPRBeiLURXX2sQDBbr9XRb73Fvm8jIOrPyJg8PcvNXH
|
||||||
|
D1jQah5K60THdjmdkLsY/hamZfqLb24EFQ==
|
||||||
|
-----END EC PRIVATE KEY-----
|
12
tests/certs/localhost/fullchain.pem
Normal file
12
tests/certs/localhost/fullchain.pem
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBwjCCAWigAwIBAgIUQOCJIPRMiZsOMmvH0uiofxEDFn8wCgYIKoZIzj0EAwIw
|
||||||
|
KDESMBAGA1UEAwwJbG9jYWxob3N0MRIwEAYDVQQKDAlTYW5pYyBPcmcwHhcNMjEx
|
||||||
|
MDE5MTcwMTE3WhcNMjkxMDE5MTcwMTE3WjAoMRIwEAYDVQQDDAlsb2NhbGhvc3Qx
|
||||||
|
EjAQBgNVBAoMCVNhbmljIE9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHf0
|
||||||
|
SrvRtGF9KIXEtk4+6vsqleNaleuYVvf4d6TD3pX1CbOV/NsZdW6+EhkA1U2pEBnJ
|
||||||
|
txXqAGVJT4ans8ud3K6jcDBuMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggr
|
||||||
|
BgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAsBgNVHREEJTAjggls
|
||||||
|
b2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0EAwIDSAAw
|
||||||
|
RQIhAJhwopVuiW0S4MKEDCl+Vxwyei5AYobrALcP0pwGpFzIAiAWkxMPeAOMWIjq
|
||||||
|
LD4t2UZ9h6ma2fS2Jf9pzTon6438Ng==
|
||||||
|
-----END CERTIFICATE-----
|
5
tests/certs/localhost/privkey.pem
Normal file
5
tests/certs/localhost/privkey.pem
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIDKTs1c2Qo7KMQ8DJrmIuNb29z2fNi4O+TNkJWjvclvsoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEd/RKu9G0YX0ohcS2Tj7q+yqV41qV65hW9/h3pMPelfUJs5X82xl1
|
||||||
|
br4SGQDVTakQGcm3FeoAZUlPhqezy53crg==
|
||||||
|
-----END EC PRIVATE KEY-----
|
21
tests/certs/sanic.example/fullchain.pem
Normal file
21
tests/certs/sanic.example/fullchain.pem
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDdzCCAl+gAwIBAgIUF1H0To9k3mUiMT8mjF6g45A9KgcwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwLDEWMBQGA1UEAwwNc2FuaWMuZXhhbXBsZTESMBAGA1UECgwJU2FuaWMgT3Jn
|
||||||
|
MB4XDTIxMTAxOTE3MDExN1oXDTI5MTAxOTE3MDExN1owLDEWMBQGA1UEAwwNc2Fu
|
||||||
|
aWMuZXhhbXBsZTESMBAGA1UECgwJU2FuaWMgT3JnMIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAzNeC95zB5LRybz9Wl16+Q4kbOLgXlyUQVKhg9OZD1ChN
|
||||||
|
3T4Ya/KvChQmPWOWdF814NgkkNS1yHKXlORU2Ljbpqzr+WoOAwGVixbRTknjmI46
|
||||||
|
glUhCOJlGqxl16RfuYA2BWv0+At9jKBhT1tnrGVhfqldnxsb4FDh0JsFnrZN4/DB
|
||||||
|
z6x8PY1z0eQMgsyeKAfSTTnGXhkZzAQz6afuQbGZhe8vQIUTvwmnZiU9OdUZ6nLc
|
||||||
|
b7lSbIQ1edT6/xXUkbn5ixGsEQTf6JWLqEDLkqpo9sbkYmvMMQpj2pCgtaEjx7An
|
||||||
|
+hQe8Itv+i6h0KD3ARVeCBgdWEgXZTs7zKrmU77xfwIDAQABo4GQMIGNMA4GA1Ud
|
||||||
|
DwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0T
|
||||||
|
AQH/BAUwAwEB/zBLBgNVHREERDBCgg1zYW5pYy5leGFtcGxlghF3d3cuc2FuaWMu
|
||||||
|
ZXhhbXBsZYIMKi5zYW5pYy50ZXN0hxAgAQ24AAAAAAAAAAAAAFQcMA0GCSqGSIb3
|
||||||
|
DQEBCwUAA4IBAQBLV7xSEI7308Qmm3SyV+ro9jQ/i2ydwUIUyRMtf04EFRS8fHK/
|
||||||
|
Lln5Yweaba9XP5k3DLSC63Qg1tE50fVqQypbWVA4SMkMW21cK8vEhHEYeGYkHsuC
|
||||||
|
xCFdwJYhmofqWaQ/j/ErLBrQbaHBdSJ/Nou5RPRtM4HrSU7F2azLGmLczYk6PcZa
|
||||||
|
wSBvoXdjiEUrRl7XB0iB2ktTga6amuYz4bSJzUvaA8SodJzC4OKhRsduUD83LdDi
|
||||||
|
2As4KiTcSO/SOCaK2KmbPNBlTKMF4cpqysGMvmnGVWhECOG1PZItJkWNbbBV4XRR
|
||||||
|
qGmrey2JwDDeTYHFDHaND385/PSJKfSSGLNk
|
||||||
|
-----END CERTIFICATE-----
|
27
tests/certs/sanic.example/privkey.pem
Normal file
27
tests/certs/sanic.example/privkey.pem
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEAzNeC95zB5LRybz9Wl16+Q4kbOLgXlyUQVKhg9OZD1ChN3T4Y
|
||||||
|
a/KvChQmPWOWdF814NgkkNS1yHKXlORU2Ljbpqzr+WoOAwGVixbRTknjmI46glUh
|
||||||
|
COJlGqxl16RfuYA2BWv0+At9jKBhT1tnrGVhfqldnxsb4FDh0JsFnrZN4/DBz6x8
|
||||||
|
PY1z0eQMgsyeKAfSTTnGXhkZzAQz6afuQbGZhe8vQIUTvwmnZiU9OdUZ6nLcb7lS
|
||||||
|
bIQ1edT6/xXUkbn5ixGsEQTf6JWLqEDLkqpo9sbkYmvMMQpj2pCgtaEjx7An+hQe
|
||||||
|
8Itv+i6h0KD3ARVeCBgdWEgXZTs7zKrmU77xfwIDAQABAoIBABWKpG89wPY4M8CX
|
||||||
|
PJf2krOve3lfgruWXj1I58lZXdC13Fpj6VWQ0++PZuYVzwC18oiOsmm4tNU7l81E
|
||||||
|
pdeUuSSyEq7MBGU0iXFzGNfO1Wx5qJWENlEk3dUMRDmFQ7vSS9wOGljrfGyJgTJD
|
||||||
|
PofWsYYMcZgF1cylNNonM1QZf990hfd0JDfO6CHCloRe/pKIdVzIxQp+3Ju/3OPk
|
||||||
|
Gw5V+YnVrG4wdZbhOCW2hPp/TLdgFy/xHvrxkEkGx+2ZHGCw9uFj2LRZJwwuaO9p
|
||||||
|
LDzbyfbFlPWIHdPamdBvenZ6RNTf28+YsbiqwoOk5C286QYb/VDnT8UnG42hXS1I
|
||||||
|
p3m//qECgYEA7zXmMSBy1tkMQsuaAakOFfl2HfVL2rrW6/CH6BwcCHUD6Wr8wv6a
|
||||||
|
kPNhI6pqqnP6Xg8XqJXfyIVZOJYPQMQr69zni2y7b3jPOemVGTBSqN7UE71NZkHF
|
||||||
|
+HZov55bPuX/KD6qc/WAXCyEcISy9TmcA7cEN7ivmyXmbuSXEoiAjlsCgYEA2zgU
|
||||||
|
mzL6ObJ2555UOqzGCMx6o2KQqOgA1SGmYLBRX77I3fuvGj+DLo6/iuM0FcVV7alG
|
||||||
|
U/U6qqrSymtdRgeZXHziSVhLZKY/qobgKG2iO1F3DzqyZ94EK/v0XRS4UyiJma3f
|
||||||
|
lwVG/BcVnv+FKCYUo2JKGln0R8Wcm6D9Nxp0mq0CgYEAn0Dj+oreyZiAqCuCYV6a
|
||||||
|
SRjmgTVghcNj+HoPEQE9zIeSziBzHKKCZsQRRLxc/RPveBVWK99zt7zHVHvatcSk
|
||||||
|
dQeBg3olIyZr1+NhZv6b2V9YE7gwwkZBtZOnUwLrPmnCwJlPw5mLFlJw7bP6rHXp
|
||||||
|
HzQF887Z4lGOIv++cBE+fQcCgYEArF26BhXdHcSvLYsWW1RCGeT9gL4dVFGnZe2h
|
||||||
|
bmD0er3+Hlyo35CUyuS+wqvG5l9VIxt4CsfFKzBJsZMdsdSDx28CVf0wuqDlamXG
|
||||||
|
lsMtTkrNvJHAeV7eFN900kNaczhqiQVnys0BdXGJNI1g26Klk5nS/klAg7ZjXxME
|
||||||
|
RnFswbkCgYBG5OToLXM8pg3yTM9MHMSXFhnnd2MbBK2AySFah2P1V4xv1rJdklU0
|
||||||
|
9QRTd/hQmYGHioPIF9deU8YSWlj+FBimyoNfJ51YzFyp2maOSJq4Wxe1nv2DflRK
|
||||||
|
gh5pkl8FizoDnu8BHu1AjOfRQJ3/tCIi2XZJgBuCxyTjd1b6hVUhyg==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
@ -1,22 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
|
||||||
BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
|
||||||
aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF
|
|
||||||
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
|
||||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
|
||||||
CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh
|
|
||||||
V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE
|
|
||||||
vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3
|
|
||||||
h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1
|
|
||||||
w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf
|
|
||||||
qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix
|
|
||||||
9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4
|
|
||||||
NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
|
|
||||||
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF
|
|
||||||
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7
|
|
||||||
ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ
|
|
||||||
7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj
|
|
||||||
teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+
|
|
||||||
mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5
|
|
||||||
zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,27 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7
|
|
||||||
mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw
|
|
||||||
dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49
|
|
||||||
IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ
|
|
||||||
8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ
|
|
||||||
PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo
|
|
||||||
7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6
|
|
||||||
VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h
|
|
||||||
4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5
|
|
||||||
th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4
|
|
||||||
56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW
|
|
||||||
TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs
|
|
||||||
80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK
|
|
||||||
gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs
|
|
||||||
WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g
|
|
||||||
vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay
|
|
||||||
mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w
|
|
||||||
bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm
|
|
||||||
fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8
|
|
||||||
0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB
|
|
||||||
8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6
|
|
||||||
vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g
|
|
||||||
mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL
|
|
||||||
sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2
|
|
||||||
gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg==
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
@ -6,7 +6,8 @@ import string
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from typing import Tuple
|
from logging import LogRecord
|
||||||
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -170,3 +171,16 @@ def run_startup(caplog):
|
|||||||
return caplog.record_tuples
|
return caplog.record_tuples
|
||||||
|
|
||||||
return run
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def message_in_records():
|
||||||
|
def msg_in_log(records: List[LogRecord], msg: str):
|
||||||
|
error_captured = False
|
||||||
|
for record in records:
|
||||||
|
if msg in record.message:
|
||||||
|
error_captured = True
|
||||||
|
break
|
||||||
|
return error_captured
|
||||||
|
|
||||||
|
return msg_in_log
|
||||||
|
@ -23,6 +23,7 @@ async def app_info_dump(app: Sanic, _):
|
|||||||
"access_log": app.config.ACCESS_LOG,
|
"access_log": app.config.ACCESS_LOG,
|
||||||
"auto_reload": app.auto_reload,
|
"auto_reload": app.auto_reload,
|
||||||
"debug": app.debug,
|
"debug": app.debug,
|
||||||
|
"noisy_exceptions": app.config.NOISY_EXCEPTIONS,
|
||||||
}
|
}
|
||||||
logger.info(json.dumps(app_data))
|
logger.info(json.dumps(app_data))
|
||||||
|
|
||||||
|
@ -39,7 +39,6 @@ def test_app_loop_running(app):
|
|||||||
|
|
||||||
|
|
||||||
def test_create_asyncio_server(app):
|
def test_create_asyncio_server(app):
|
||||||
if not uvloop_installed():
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
|
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
|
||||||
assert isawaitable(asyncio_srv_coro)
|
assert isawaitable(asyncio_srv_coro)
|
||||||
@ -48,7 +47,6 @@ def test_create_asyncio_server(app):
|
|||||||
|
|
||||||
|
|
||||||
def test_asyncio_server_no_start_serving(app):
|
def test_asyncio_server_no_start_serving(app):
|
||||||
if not uvloop_installed():
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
asyncio_srv_coro = app.create_server(
|
asyncio_srv_coro = app.create_server(
|
||||||
port=43123,
|
port=43123,
|
||||||
@ -60,7 +58,6 @@ def test_asyncio_server_no_start_serving(app):
|
|||||||
|
|
||||||
|
|
||||||
def test_asyncio_server_start_serving(app):
|
def test_asyncio_server_start_serving(app):
|
||||||
if not uvloop_installed():
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
asyncio_srv_coro = app.create_server(
|
asyncio_srv_coro = app.create_server(
|
||||||
port=43124,
|
port=43124,
|
||||||
@ -69,6 +66,7 @@ def test_asyncio_server_start_serving(app):
|
|||||||
)
|
)
|
||||||
srv = loop.run_until_complete(asyncio_srv_coro)
|
srv = loop.run_until_complete(asyncio_srv_coro)
|
||||||
assert srv.is_serving() is False
|
assert srv.is_serving() is False
|
||||||
|
loop.run_until_complete(srv.startup())
|
||||||
loop.run_until_complete(srv.start_serving())
|
loop.run_until_complete(srv.start_serving())
|
||||||
assert srv.is_serving() is True
|
assert srv.is_serving() is True
|
||||||
wait_close = srv.close()
|
wait_close = srv.close()
|
||||||
@ -90,6 +88,21 @@ def test_create_server_main(app, caplog):
|
|||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_server_no_startup(app):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
asyncio_srv_coro = app.create_server(
|
||||||
|
port=43124,
|
||||||
|
return_asyncio_server=True,
|
||||||
|
asyncio_server_kwargs=dict(start_serving=False),
|
||||||
|
)
|
||||||
|
srv = loop.run_until_complete(asyncio_srv_coro)
|
||||||
|
message = (
|
||||||
|
"Cannot run Sanic server without first running await server.startup()"
|
||||||
|
)
|
||||||
|
with pytest.raises(SanicException, match=message):
|
||||||
|
loop.run_until_complete(srv.start_serving())
|
||||||
|
|
||||||
|
|
||||||
def test_create_server_main_convenience(app, caplog):
|
def test_create_server_main_convenience(app, caplog):
|
||||||
app.main_process_start(lambda *_: ...)
|
app.main_process_start(lambda *_: ...)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
@ -104,6 +117,19 @@ def test_create_server_main_convenience(app, caplog):
|
|||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_server_init(app, caplog):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
|
||||||
|
server = loop.run_until_complete(asyncio_srv_coro)
|
||||||
|
|
||||||
|
message = (
|
||||||
|
"AsyncioServer.init has been deprecated and will be removed in v22.6. "
|
||||||
|
"Use Sanic.state.is_started instead."
|
||||||
|
)
|
||||||
|
with pytest.warns(DeprecationWarning, match=message):
|
||||||
|
server.init
|
||||||
|
|
||||||
|
|
||||||
def test_app_loop_not_running(app):
|
def test_app_loop_not_running(app):
|
||||||
with pytest.raises(SanicException) as excinfo:
|
with pytest.raises(SanicException) as excinfo:
|
||||||
app.loop
|
app.loop
|
||||||
@ -444,3 +470,9 @@ def test_custom_context():
|
|||||||
app = Sanic("custom", ctx=ctx)
|
app = Sanic("custom", ctx=ctx)
|
||||||
|
|
||||||
assert app.ctx == ctx
|
assert app.ctx == ctx
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_run_fast_and_workers(app):
|
||||||
|
message = "You cannot use both fast=True and workers=X"
|
||||||
|
with pytest.raises(RuntimeError, match=message):
|
||||||
|
app.run(fast=True, workers=4)
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
from copy import deepcopy
|
from sanic import Blueprint, Sanic
|
||||||
|
|
||||||
from sanic import Blueprint, Sanic, blueprints, response
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
@ -1088,3 +1088,31 @@ def test_bp_set_attribute_warning():
|
|||||||
"and will be removed in version 21.12. You should change your "
|
"and will be removed in version 21.12. You should change your "
|
||||||
"Blueprint instance to use instance.ctx.foo instead."
|
"Blueprint instance to use instance.ctx.foo instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_early_registration(app):
|
||||||
|
assert len(app.router.routes) == 0
|
||||||
|
|
||||||
|
bp = Blueprint("bp")
|
||||||
|
|
||||||
|
@bp.get("/one")
|
||||||
|
async def one(_):
|
||||||
|
return text("one")
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
assert len(app.router.routes) == 1
|
||||||
|
|
||||||
|
@bp.get("/two")
|
||||||
|
async def two(_):
|
||||||
|
return text("two")
|
||||||
|
|
||||||
|
@bp.get("/three")
|
||||||
|
async def three(_):
|
||||||
|
return text("three")
|
||||||
|
|
||||||
|
assert len(app.router.routes) == 3
|
||||||
|
|
||||||
|
for path in ("one", "two", "three"):
|
||||||
|
_, response = app.test_client.get(f"/{path}")
|
||||||
|
assert response.text == path
|
||||||
|
@ -8,7 +8,6 @@ import pytest
|
|||||||
from sanic_routing import __version__ as __routing_version__
|
from sanic_routing import __version__ as __routing_version__
|
||||||
|
|
||||||
from sanic import __version__
|
from sanic import __version__
|
||||||
from sanic.config import BASE_LOGO
|
|
||||||
|
|
||||||
|
|
||||||
def capture(command):
|
def capture(command):
|
||||||
@ -19,13 +18,20 @@ def capture(command):
|
|||||||
cwd=Path(__file__).parent,
|
cwd=Path(__file__).parent,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
out, err = proc.communicate(timeout=0.5)
|
out, err = proc.communicate(timeout=1)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
proc.kill()
|
proc.kill()
|
||||||
out, err = proc.communicate()
|
out, err = proc.communicate()
|
||||||
return out, err, proc.returncode
|
return out, err, proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def starting_line(lines):
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
if line.strip().startswith(b"Sanic v"):
|
||||||
|
return idx
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"appname",
|
"appname",
|
||||||
(
|
(
|
||||||
@ -39,12 +45,62 @@ def test_server_run(appname):
|
|||||||
command = ["sanic", appname]
|
command = ["sanic", appname]
|
||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
firstline = lines[6]
|
firstline = lines[starting_line(lines) + 1]
|
||||||
|
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
|
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cmd",
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"--cert=certs/sanic.example/fullchain.pem",
|
||||||
|
"--key=certs/sanic.example/privkey.pem",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"--tls=certs/sanic.example/",
|
||||||
|
"--tls=certs/localhost/",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"--tls=certs/sanic.example/",
|
||||||
|
"--tls=certs/localhost/",
|
||||||
|
"--tls-strict-host",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_tls_options(cmd):
|
||||||
|
command = ["sanic", "fake.server.app", *cmd, "-p=9999", "--debug"]
|
||||||
|
out, err, exitcode = capture(command)
|
||||||
|
assert exitcode != 1
|
||||||
|
lines = out.split(b"\n")
|
||||||
|
firstline = lines[starting_line(lines) + 1]
|
||||||
|
assert firstline == b"Goin' Fast @ https://127.0.0.1:9999"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cmd",
|
||||||
|
(
|
||||||
|
("--cert=certs/sanic.example/fullchain.pem",),
|
||||||
|
(
|
||||||
|
"--cert=certs/sanic.example/fullchain.pem",
|
||||||
|
"--key=certs/sanic.example/privkey.pem",
|
||||||
|
"--tls=certs/localhost/",
|
||||||
|
),
|
||||||
|
("--tls-strict-host",),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_tls_wrong_options(cmd):
|
||||||
|
command = ["sanic", "fake.server.app", *cmd, "-p=9999", "--debug"]
|
||||||
|
out, err, exitcode = capture(command)
|
||||||
|
assert exitcode == 1
|
||||||
|
assert not out
|
||||||
|
lines = err.decode().split("\n")
|
||||||
|
|
||||||
|
errmsg = lines[8]
|
||||||
|
assert errmsg == "TLS certificates must be specified by either of:"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"cmd",
|
"cmd",
|
||||||
(
|
(
|
||||||
@ -52,16 +108,67 @@ def test_server_run(appname):
|
|||||||
("-H", "localhost", "-p", "9999"),
|
("-H", "localhost", "-p", "9999"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_host_port(cmd):
|
def test_host_port_localhost(cmd):
|
||||||
command = ["sanic", "fake.server.app", *cmd]
|
command = ["sanic", "fake.server.app", *cmd]
|
||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
firstline = lines[6]
|
firstline = lines[starting_line(lines) + 1]
|
||||||
|
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
assert firstline == b"Goin' Fast @ http://localhost:9999"
|
assert firstline == b"Goin' Fast @ http://localhost:9999"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cmd",
|
||||||
|
(
|
||||||
|
("--host=127.0.0.127", "--port=9999"),
|
||||||
|
("-H", "127.0.0.127", "-p", "9999"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_host_port_ipv4(cmd):
|
||||||
|
command = ["sanic", "fake.server.app", *cmd]
|
||||||
|
out, err, exitcode = capture(command)
|
||||||
|
lines = out.split(b"\n")
|
||||||
|
firstline = lines[starting_line(lines) + 1]
|
||||||
|
|
||||||
|
assert exitcode != 1
|
||||||
|
assert firstline == b"Goin' Fast @ http://127.0.0.127:9999"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cmd",
|
||||||
|
(
|
||||||
|
("--host=::", "--port=9999"),
|
||||||
|
("-H", "::", "-p", "9999"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_host_port_ipv6_any(cmd):
|
||||||
|
command = ["sanic", "fake.server.app", *cmd]
|
||||||
|
out, err, exitcode = capture(command)
|
||||||
|
lines = out.split(b"\n")
|
||||||
|
firstline = lines[starting_line(lines) + 1]
|
||||||
|
|
||||||
|
assert exitcode != 1
|
||||||
|
assert firstline == b"Goin' Fast @ http://[::]:9999"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cmd",
|
||||||
|
(
|
||||||
|
("--host=::1", "--port=9999"),
|
||||||
|
("-H", "::1", "-p", "9999"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_host_port_ipv6_loopback(cmd):
|
||||||
|
command = ["sanic", "fake.server.app", *cmd]
|
||||||
|
out, err, exitcode = capture(command)
|
||||||
|
lines = out.split(b"\n")
|
||||||
|
firstline = lines[starting_line(lines) + 1]
|
||||||
|
|
||||||
|
assert exitcode != 1
|
||||||
|
assert firstline == b"Goin' Fast @ http://[::1]:9999"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"num,cmd",
|
"num,cmd",
|
||||||
(
|
(
|
||||||
@ -78,9 +185,13 @@ def test_num_workers(num, cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
|
|
||||||
worker_lines = [line for line in lines if b"worker" in line]
|
worker_lines = [
|
||||||
|
line
|
||||||
|
for line in lines
|
||||||
|
if b"Starting worker" in line or b"Stopping worker" in line
|
||||||
|
]
|
||||||
assert exitcode != 1
|
assert exitcode != 1
|
||||||
assert len(worker_lines) == num * 2
|
assert len(worker_lines) == num * 2, f"Lines found: {lines}"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("cmd", ("--debug", "-d"))
|
@pytest.mark.parametrize("cmd", ("--debug", "-d"))
|
||||||
@ -89,10 +200,9 @@ def test_debug(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
|
|
||||||
app_info = lines[26]
|
app_info = lines[starting_line(lines) + 9]
|
||||||
info = json.loads(app_info)
|
info = json.loads(app_info)
|
||||||
|
|
||||||
assert (b"\n".join(lines[:6])).decode("utf-8") == BASE_LOGO
|
|
||||||
assert info["debug"] is True
|
assert info["debug"] is True
|
||||||
assert info["auto_reload"] is True
|
assert info["auto_reload"] is True
|
||||||
|
|
||||||
@ -103,7 +213,7 @@ def test_auto_reload(cmd):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
|
|
||||||
app_info = lines[26]
|
app_info = lines[starting_line(lines) + 9]
|
||||||
info = json.loads(app_info)
|
info = json.loads(app_info)
|
||||||
|
|
||||||
assert info["debug"] is False
|
assert info["debug"] is False
|
||||||
@ -118,7 +228,7 @@ def test_access_logs(cmd, expected):
|
|||||||
out, err, exitcode = capture(command)
|
out, err, exitcode = capture(command)
|
||||||
lines = out.split(b"\n")
|
lines = out.split(b"\n")
|
||||||
|
|
||||||
app_info = lines[26]
|
app_info = lines[starting_line(lines) + 8]
|
||||||
info = json.loads(app_info)
|
info = json.loads(app_info)
|
||||||
|
|
||||||
assert info["access_log"] is expected
|
assert info["access_log"] is expected
|
||||||
@ -131,3 +241,21 @@ def test_version(cmd):
|
|||||||
version_string = f"Sanic {__version__}; Routing {__routing_version__}\n"
|
version_string = f"Sanic {__version__}; Routing {__routing_version__}\n"
|
||||||
|
|
||||||
assert out == version_string.encode("utf-8")
|
assert out == version_string.encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cmd,expected",
|
||||||
|
(
|
||||||
|
("--noisy-exceptions", True),
|
||||||
|
("--no-noisy-exceptions", False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_noisy_exceptions(cmd, expected):
|
||||||
|
command = ["sanic", "fake.server.app", cmd]
|
||||||
|
out, err, exitcode = capture(command)
|
||||||
|
lines = out.split(b"\n")
|
||||||
|
|
||||||
|
app_info = lines[starting_line(lines) + 8]
|
||||||
|
info = json.loads(app_info)
|
||||||
|
|
||||||
|
assert info["noisy_exceptions"] is expected
|
||||||
|
48
tests/test_coffee.py
Normal file
48
tests/test_coffee.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic.application.logo import COFFEE_LOGO, get_logo
|
||||||
|
from sanic.exceptions import SanicException
|
||||||
|
|
||||||
|
|
||||||
|
def has_sugar(value):
|
||||||
|
if value:
|
||||||
|
raise SanicException("I said no sugar please")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sugar", (True, False))
|
||||||
|
def test_no_sugar(sugar):
|
||||||
|
if sugar:
|
||||||
|
with pytest.raises(SanicException):
|
||||||
|
assert has_sugar(sugar)
|
||||||
|
else:
|
||||||
|
assert not has_sugar(sugar)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_logo_returns_expected_logo():
|
||||||
|
with patch("sys.stdout.isatty") as isatty:
|
||||||
|
isatty.return_value = True
|
||||||
|
logo = get_logo(coffee=True)
|
||||||
|
assert logo is COFFEE_LOGO
|
||||||
|
|
||||||
|
|
||||||
|
def test_logo_true(app, caplog):
|
||||||
|
@app.after_server_start
|
||||||
|
async def shutdown(*_):
|
||||||
|
app.stop()
|
||||||
|
|
||||||
|
with patch("sys.stdout.isatty") as isatty:
|
||||||
|
isatty.return_value = True
|
||||||
|
with caplog.at_level(logging.DEBUG):
|
||||||
|
app.make_coffee()
|
||||||
|
|
||||||
|
# Only in the regular logo
|
||||||
|
assert " ▄███ █████ ██ " not in caplog.text
|
||||||
|
|
||||||
|
# Only in the coffee logo
|
||||||
|
assert " ██ ██▀▀▄ " in caplog.text
|
@ -3,6 +3,7 @@ from os import environ
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -350,3 +351,40 @@ def test_update_from_lowercase_key(app):
|
|||||||
d = {"test_setting_value": 1}
|
d = {"test_setting_value": 1}
|
||||||
app.update_config(d)
|
app.update_config(d)
|
||||||
assert "test_setting_value" not in app.config
|
assert "test_setting_value" not in app.config
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecation_notice_when_setting_logo(app):
|
||||||
|
message = (
|
||||||
|
"Setting the config.LOGO is deprecated and will no longer be "
|
||||||
|
"supported starting in v22.6."
|
||||||
|
)
|
||||||
|
with pytest.warns(DeprecationWarning, match=message):
|
||||||
|
app.config.LOGO = "My Custom Logo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_set_methods(app, monkeypatch):
|
||||||
|
post_set = Mock()
|
||||||
|
monkeypatch.setattr(Config, "_post_set", post_set)
|
||||||
|
|
||||||
|
app.config.FOO = 1
|
||||||
|
post_set.assert_called_once_with("FOO", 1)
|
||||||
|
post_set.reset_mock()
|
||||||
|
|
||||||
|
app.config["FOO"] = 2
|
||||||
|
post_set.assert_called_once_with("FOO", 2)
|
||||||
|
post_set.reset_mock()
|
||||||
|
|
||||||
|
app.config.update({"FOO": 3})
|
||||||
|
post_set.assert_called_once_with("FOO", 3)
|
||||||
|
post_set.reset_mock()
|
||||||
|
|
||||||
|
app.config.update([("FOO", 4)])
|
||||||
|
post_set.assert_called_once_with("FOO", 4)
|
||||||
|
post_set.reset_mock()
|
||||||
|
|
||||||
|
app.config.update(FOO=5)
|
||||||
|
post_set.assert_called_once_with("FOO", 5)
|
||||||
|
post_set.reset_mock()
|
||||||
|
|
||||||
|
app.config.update_config({"FOO": 6})
|
||||||
|
post_set.assert_called_once_with("FOO", 6)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from sanic.config import Config
|
||||||
from sanic.errorpages import HTMLRenderer, exception_response
|
from sanic.errorpages import HTMLRenderer, exception_response
|
||||||
from sanic.exceptions import NotFound, SanicException
|
from sanic.exceptions import NotFound, SanicException
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import HTTPResponse, html, json, text
|
from sanic.response import HTTPResponse, html, json, text
|
||||||
|
|
||||||
@ -271,3 +273,72 @@ def test_combinations_for_auto(fake_request, accept, content_type, expected):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.content_type == expected
|
assert response.content_type == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_allow_fallback_error_format_set_main_process_start(app):
|
||||||
|
@app.main_process_start
|
||||||
|
async def start(app, _):
|
||||||
|
app.config.FALLBACK_ERROR_FORMAT = "text"
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/error")
|
||||||
|
assert request.app.error_handler.fallback == "text"
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def test_setting_fallback_to_non_default_raise_warning(app):
|
||||||
|
app.error_handler = ErrorHandler(fallback="text")
|
||||||
|
|
||||||
|
assert app.error_handler.fallback == "text"
|
||||||
|
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match=(
|
||||||
|
"Overriding non-default ErrorHandler fallback value. "
|
||||||
|
"Changing from text to auto."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
app.config.FALLBACK_ERROR_FORMAT = "auto"
|
||||||
|
|
||||||
|
assert app.error_handler.fallback == "auto"
|
||||||
|
|
||||||
|
app.config.FALLBACK_ERROR_FORMAT = "text"
|
||||||
|
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match=(
|
||||||
|
"Overriding non-default ErrorHandler fallback value. "
|
||||||
|
"Changing from text to json."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
app.config.FALLBACK_ERROR_FORMAT = "json"
|
||||||
|
|
||||||
|
assert app.error_handler.fallback == "json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_allow_fallback_error_format_in_config_injection():
|
||||||
|
class MyConfig(Config):
|
||||||
|
FALLBACK_ERROR_FORMAT = "text"
|
||||||
|
|
||||||
|
app = Sanic("test", config=MyConfig())
|
||||||
|
|
||||||
|
@app.route("/error", methods=["GET", "POST"])
|
||||||
|
def err(request):
|
||||||
|
raise Exception("something went wrong")
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/error")
|
||||||
|
assert request.app.error_handler.fallback == "text"
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def test_allow_fallback_error_format_in_config_replacement(app):
|
||||||
|
class MyConfig(Config):
|
||||||
|
FALLBACK_ERROR_FORMAT = "text"
|
||||||
|
|
||||||
|
app.config = MyConfig()
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/error")
|
||||||
|
assert request.app.error_handler.fallback == "text"
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
@ -4,7 +4,6 @@ import warnings
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from websockets.version import version as websockets_version
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
@ -19,6 +18,16 @@ from sanic.exceptions import (
|
|||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
def dl_to_dict(soup, css_class):
|
||||||
|
keys, values = [], []
|
||||||
|
for dl in soup.find_all("dl", {"class": css_class}):
|
||||||
|
for dt in dl.find_all("dt"):
|
||||||
|
keys.append(dt.text.strip())
|
||||||
|
for dd in dl.find_all("dd"):
|
||||||
|
values.append(dd.text.strip())
|
||||||
|
return dict(zip(keys, values))
|
||||||
|
|
||||||
|
|
||||||
class SanicExceptionTestException(Exception):
|
class SanicExceptionTestException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -261,14 +270,114 @@ def test_exception_in_ws_logged(caplog):
|
|||||||
|
|
||||||
with caplog.at_level(logging.INFO):
|
with caplog.at_level(logging.INFO):
|
||||||
app.test_client.websocket("/feed")
|
app.test_client.websocket("/feed")
|
||||||
# Websockets v10.0 and above output an additional
|
|
||||||
# INFO message when a ws connection is accepted
|
error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"]
|
||||||
ws_version_parts = websockets_version.split(".")
|
assert error_logs[1][1] == logging.ERROR
|
||||||
ws_major = int(ws_version_parts[0])
|
assert "Exception occurred while handling uri:" in error_logs[1][2]
|
||||||
record_index = 2 if ws_major >= 10 else 1
|
|
||||||
assert caplog.record_tuples[record_index][0] == "sanic.error"
|
|
||||||
assert caplog.record_tuples[record_index][1] == logging.ERROR
|
@pytest.mark.parametrize("debug", (True, False))
|
||||||
assert (
|
def test_contextual_exception_context(debug):
|
||||||
"Exception occurred while handling uri:"
|
app = Sanic(__name__)
|
||||||
in caplog.record_tuples[record_index][2]
|
|
||||||
)
|
class TeapotError(SanicException):
|
||||||
|
status_code = 418
|
||||||
|
message = "Sorry, I cannot brew coffee"
|
||||||
|
|
||||||
|
def fail():
|
||||||
|
raise TeapotError(context={"foo": "bar"})
|
||||||
|
|
||||||
|
app.post("/coffee/json", error_format="json")(lambda _: fail())
|
||||||
|
app.post("/coffee/html", error_format="html")(lambda _: fail())
|
||||||
|
app.post("/coffee/text", error_format="text")(lambda _: fail())
|
||||||
|
|
||||||
|
_, response = app.test_client.post("/coffee/json", debug=debug)
|
||||||
|
assert response.status == 418
|
||||||
|
assert response.json["message"] == "Sorry, I cannot brew coffee"
|
||||||
|
assert response.json["context"] == {"foo": "bar"}
|
||||||
|
|
||||||
|
_, response = app.test_client.post("/coffee/html", debug=debug)
|
||||||
|
soup = BeautifulSoup(response.body, "html.parser")
|
||||||
|
dl = dl_to_dict(soup, "context")
|
||||||
|
assert response.status == 418
|
||||||
|
assert "Sorry, I cannot brew coffee" in soup.find("p").text
|
||||||
|
assert dl == {"foo": "bar"}
|
||||||
|
|
||||||
|
_, response = app.test_client.post("/coffee/text", debug=debug)
|
||||||
|
lines = list(map(lambda x: x.decode(), response.body.split(b"\n")))
|
||||||
|
idx = lines.index("Context") + 1
|
||||||
|
assert response.status == 418
|
||||||
|
assert lines[2] == "Sorry, I cannot brew coffee"
|
||||||
|
assert lines[idx] == ' foo: "bar"'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("debug", (True, False))
|
||||||
|
def test_contextual_exception_extra(debug):
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
class TeapotError(SanicException):
|
||||||
|
status_code = 418
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return f"Found {self.extra['foo']}"
|
||||||
|
|
||||||
|
def fail():
|
||||||
|
raise TeapotError(extra={"foo": "bar"})
|
||||||
|
|
||||||
|
app.post("/coffee/json", error_format="json")(lambda _: fail())
|
||||||
|
app.post("/coffee/html", error_format="html")(lambda _: fail())
|
||||||
|
app.post("/coffee/text", error_format="text")(lambda _: fail())
|
||||||
|
|
||||||
|
_, response = app.test_client.post("/coffee/json", debug=debug)
|
||||||
|
assert response.status == 418
|
||||||
|
assert response.json["message"] == "Found bar"
|
||||||
|
if debug:
|
||||||
|
assert response.json["extra"] == {"foo": "bar"}
|
||||||
|
else:
|
||||||
|
assert "extra" not in response.json
|
||||||
|
|
||||||
|
_, response = app.test_client.post("/coffee/html", debug=debug)
|
||||||
|
soup = BeautifulSoup(response.body, "html.parser")
|
||||||
|
dl = dl_to_dict(soup, "extra")
|
||||||
|
assert response.status == 418
|
||||||
|
assert "Found bar" in soup.find("p").text
|
||||||
|
if debug:
|
||||||
|
assert dl == {"foo": "bar"}
|
||||||
|
else:
|
||||||
|
assert not dl
|
||||||
|
|
||||||
|
_, response = app.test_client.post("/coffee/text", debug=debug)
|
||||||
|
lines = list(map(lambda x: x.decode(), response.body.split(b"\n")))
|
||||||
|
assert response.status == 418
|
||||||
|
assert lines[2] == "Found bar"
|
||||||
|
if debug:
|
||||||
|
idx = lines.index("Extra") + 1
|
||||||
|
assert lines[idx] == ' foo: "bar"'
|
||||||
|
else:
|
||||||
|
assert "Extra" not in lines
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("override", (True, False))
|
||||||
|
def test_contextual_exception_functional_message(override):
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
class TeapotError(SanicException):
|
||||||
|
status_code = 418
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return f"Received foo={self.context['foo']}"
|
||||||
|
|
||||||
|
@app.post("/coffee", error_format="json")
|
||||||
|
async def make_coffee(_):
|
||||||
|
error_args = {"context": {"foo": "bar"}}
|
||||||
|
if override:
|
||||||
|
error_args["message"] = "override"
|
||||||
|
raise TeapotError(**error_args)
|
||||||
|
|
||||||
|
_, response = app.test_client.post("/coffee", debug=True)
|
||||||
|
error_message = "override" if override else "Received foo=bar"
|
||||||
|
assert response.status == 418
|
||||||
|
assert response.json["message"] == error_message
|
||||||
|
assert response.json["context"] == {"foo": "bar"}
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from typing import Callable, List
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
from pytest import LogCaptureFixture, MonkeyPatch
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic, handlers
|
||||||
from sanic.exceptions import Forbidden, InvalidUsage, NotFound, ServerError
|
from sanic.exceptions import Forbidden, InvalidUsage, NotFound, ServerError
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
|
from sanic.request import Request
|
||||||
from sanic.response import stream, text
|
from sanic.response import stream, text
|
||||||
|
|
||||||
|
|
||||||
@ -88,35 +93,35 @@ def exception_handler_app():
|
|||||||
return exception_handler_app
|
return exception_handler_app
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_usage_exception_handler(exception_handler_app):
|
def test_invalid_usage_exception_handler(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get("/1")
|
request, response = exception_handler_app.test_client.get("/1")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
def test_server_error_exception_handler(exception_handler_app):
|
def test_server_error_exception_handler(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get("/2")
|
request, response = exception_handler_app.test_client.get("/2")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
|
|
||||||
|
|
||||||
def test_not_found_exception_handler(exception_handler_app):
|
def test_not_found_exception_handler(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get("/3")
|
request, response = exception_handler_app.test_client.get("/3")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
def test_text_exception__handler(exception_handler_app):
|
def test_text_exception__handler(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get("/random")
|
request, response = exception_handler_app.test_client.get("/random")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "Done."
|
assert response.text == "Done."
|
||||||
|
|
||||||
|
|
||||||
def test_async_exception_handler(exception_handler_app):
|
def test_async_exception_handler(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get("/7")
|
request, response = exception_handler_app.test_client.get("/7")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "foo,bar"
|
assert response.text == "foo,bar"
|
||||||
|
|
||||||
|
|
||||||
def test_html_traceback_output_in_debug_mode(exception_handler_app):
|
def test_html_traceback_output_in_debug_mode(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get("/4", debug=True)
|
request, response = exception_handler_app.test_client.get("/4", debug=True)
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
soup = BeautifulSoup(response.body, "html.parser")
|
soup = BeautifulSoup(response.body, "html.parser")
|
||||||
@ -131,12 +136,12 @@ def test_html_traceback_output_in_debug_mode(exception_handler_app):
|
|||||||
) == summary_text
|
) == summary_text
|
||||||
|
|
||||||
|
|
||||||
def test_inherited_exception_handler(exception_handler_app):
|
def test_inherited_exception_handler(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get("/5")
|
request, response = exception_handler_app.test_client.get("/5")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
def test_chained_exception_handler(exception_handler_app):
|
def test_chained_exception_handler(exception_handler_app: Sanic):
|
||||||
request, response = exception_handler_app.test_client.get(
|
request, response = exception_handler_app.test_client.get(
|
||||||
"/6/0", debug=True
|
"/6/0", debug=True
|
||||||
)
|
)
|
||||||
@ -155,7 +160,7 @@ def test_chained_exception_handler(exception_handler_app):
|
|||||||
) == summary_text
|
) == summary_text
|
||||||
|
|
||||||
|
|
||||||
def test_exception_handler_lookup(exception_handler_app):
|
def test_exception_handler_lookup(exception_handler_app: Sanic):
|
||||||
class CustomError(Exception):
|
class CustomError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -203,27 +208,92 @@ def test_exception_handler_lookup(exception_handler_app):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_exception_handler_processed_request_middleware(exception_handler_app):
|
def test_exception_handler_processed_request_middleware(
|
||||||
|
exception_handler_app: Sanic,
|
||||||
|
):
|
||||||
request, response = exception_handler_app.test_client.get("/8")
|
request, response = exception_handler_app.test_client.get("/8")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "Done."
|
assert response.text == "Done."
|
||||||
|
|
||||||
|
|
||||||
def test_single_arg_exception_handler_notice(exception_handler_app, caplog):
|
def test_single_arg_exception_handler_notice(
|
||||||
|
exception_handler_app: Sanic, caplog: LogCaptureFixture
|
||||||
|
):
|
||||||
class CustomErrorHandler(ErrorHandler):
|
class CustomErrorHandler(ErrorHandler):
|
||||||
def lookup(self, exception):
|
def lookup(self, exception):
|
||||||
return super().lookup(exception, None)
|
return super().lookup(exception, None)
|
||||||
|
|
||||||
exception_handler_app.error_handler = CustomErrorHandler()
|
exception_handler_app.error_handler = CustomErrorHandler()
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING):
|
message = (
|
||||||
_, response = exception_handler_app.test_client.get("/1")
|
|
||||||
|
|
||||||
assert caplog.records[0].message == (
|
|
||||||
"You are using a deprecated error handler. The lookup method should "
|
"You are using a deprecated error handler. The lookup method should "
|
||||||
"accept two positional parameters: (exception, route_name: "
|
"accept two positional parameters: (exception, route_name: "
|
||||||
"Optional[str]). Until you upgrade your ErrorHandler.lookup, "
|
"Optional[str]). Until you upgrade your ErrorHandler.lookup, "
|
||||||
"Blueprint specific exceptions will not work properly. Beginning in "
|
"Blueprint specific exceptions will not work properly. Beginning in "
|
||||||
"v22.3, the legacy style lookup method will not work at all."
|
"v22.3, the legacy style lookup method will not work at all."
|
||||||
)
|
)
|
||||||
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
|
_, response = exception_handler_app.test_client.get("/1")
|
||||||
|
|
||||||
|
assert len(record) == 1
|
||||||
|
assert record[0].message.args[0] == message
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_handler_noisy_log(
|
||||||
|
exception_handler_app: Sanic, monkeypatch: MonkeyPatch
|
||||||
|
):
|
||||||
|
err_logger = Mock()
|
||||||
|
monkeypatch.setattr(handlers, "error_logger", err_logger)
|
||||||
|
|
||||||
|
exception_handler_app.config["NOISY_EXCEPTIONS"] = False
|
||||||
|
exception_handler_app.test_client.get("/1")
|
||||||
|
err_logger.exception.assert_not_called()
|
||||||
|
|
||||||
|
exception_handler_app.config["NOISY_EXCEPTIONS"] = True
|
||||||
|
request, _ = exception_handler_app.test_client.get("/1")
|
||||||
|
err_logger.exception.assert_called_with(
|
||||||
|
"Exception occurred while handling uri: %s", repr(request.url)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exception_handler_response_was_sent(
|
||||||
|
app: Sanic,
|
||||||
|
caplog: LogCaptureFixture,
|
||||||
|
message_in_records: Callable[[List[logging.LogRecord], str], bool],
|
||||||
|
):
|
||||||
|
exception_handler_ran = False
|
||||||
|
|
||||||
|
@app.exception(ServerError)
|
||||||
|
async def exception_handler(request, exception):
|
||||||
|
nonlocal exception_handler_ran
|
||||||
|
exception_handler_ran = True
|
||||||
|
return text("Error")
|
||||||
|
|
||||||
|
@app.route("/1")
|
||||||
|
async def handler1(request: Request):
|
||||||
|
response = await request.respond()
|
||||||
|
await response.send("some text")
|
||||||
|
raise ServerError("Exception")
|
||||||
|
|
||||||
|
@app.route("/2")
|
||||||
|
async def handler2(request: Request):
|
||||||
|
response = await request.respond()
|
||||||
|
raise ServerError("Exception")
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
_, response = app.test_client.get("/1")
|
||||||
|
assert "some text" in response.text
|
||||||
|
|
||||||
|
# Change to assert warning not in the records in the future version.
|
||||||
|
message_in_records(
|
||||||
|
caplog.records,
|
||||||
|
(
|
||||||
|
"An error occurred while handling the request after at "
|
||||||
|
"least some part of the response was sent to the client. "
|
||||||
|
"Therefore, the response from your custom exception "
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/2")
|
||||||
|
assert "Error" in response.text
|
||||||
|
@ -38,9 +38,9 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog):
|
|||||||
|
|
||||||
counter = Counter([r[1] for r in caplog.record_tuples])
|
counter = Counter([r[1] for r in caplog.record_tuples])
|
||||||
|
|
||||||
assert counter[logging.INFO] == 5
|
assert counter[logging.INFO] == 11
|
||||||
assert logging.ERROR not in counter
|
assert logging.ERROR not in counter
|
||||||
assert (
|
assert (
|
||||||
caplog.record_tuples[3][2]
|
caplog.record_tuples[9][2]
|
||||||
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
|
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
@ -9,12 +7,9 @@ from unittest.mock import Mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic_testing.testing import SanicTestClient
|
|
||||||
|
|
||||||
import sanic
|
import sanic
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.compat import OS_IS_WINDOWS
|
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@ -155,56 +150,6 @@ async def test_logger(caplog):
|
|||||||
assert record in caplog.record_tuples
|
assert record in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
|
||||||
OS_IS_WINDOWS and sys.version_info >= (3, 8),
|
|
||||||
reason="Not testable with current client",
|
|
||||||
)
|
|
||||||
def test_logger_static_and_secure(caplog):
|
|
||||||
# Same as test_logger, except for more coverage:
|
|
||||||
# - test_client initialised separately for static port
|
|
||||||
# - using ssl
|
|
||||||
rand_string = str(uuid.uuid4())
|
|
||||||
|
|
||||||
app = Sanic(name=__name__)
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
def log_info(request):
|
|
||||||
logger.info(rand_string)
|
|
||||||
return text("hello")
|
|
||||||
|
|
||||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
ssl_cert = os.path.join(current_dir, "certs/selfsigned.cert")
|
|
||||||
ssl_key = os.path.join(current_dir, "certs/selfsigned.key")
|
|
||||||
|
|
||||||
ssl_dict = {"cert": ssl_cert, "key": ssl_key}
|
|
||||||
|
|
||||||
test_client = SanicTestClient(app, port=42101)
|
|
||||||
with caplog.at_level(logging.INFO):
|
|
||||||
request, response = test_client.get(
|
|
||||||
f"https://127.0.0.1:{test_client.port}/",
|
|
||||||
server_kwargs=dict(ssl=ssl_dict),
|
|
||||||
)
|
|
||||||
|
|
||||||
port = test_client.port
|
|
||||||
|
|
||||||
assert caplog.record_tuples[0] == (
|
|
||||||
"sanic.root",
|
|
||||||
logging.INFO,
|
|
||||||
f"Goin' Fast @ https://127.0.0.1:{port}",
|
|
||||||
)
|
|
||||||
assert caplog.record_tuples[1] == (
|
|
||||||
"sanic.root",
|
|
||||||
logging.INFO,
|
|
||||||
f"https://127.0.0.1:{port}/",
|
|
||||||
)
|
|
||||||
assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string)
|
|
||||||
assert caplog.record_tuples[-1] == (
|
|
||||||
"sanic.root",
|
|
||||||
logging.INFO,
|
|
||||||
"Server Stopped",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_logging_modified_root_logger_config():
|
def test_logging_modified_root_logger_config():
|
||||||
# reset_logging()
|
# reset_logging()
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user