Compare commits
1 Commits
smoother-p
...
start-rest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa864f0bab |
16
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
16
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -21,14 +21,7 @@ body:
|
|||||||
id: code
|
id: code
|
||||||
attributes:
|
attributes:
|
||||||
label: Code snippet
|
label: Code snippet
|
||||||
description: |
|
description: Relevant source code, make sure to remove what is not necessary.
|
||||||
Relevant source code, make sure to remove what is not necessary. Please try and format your code so that it is easier to read. For example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
|
||||||
```
|
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -49,16 +42,11 @@ body:
|
|||||||
- ASGI
|
- ASGI
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: input
|
||||||
id: os
|
id: os
|
||||||
attributes:
|
attributes:
|
||||||
label: Operating System
|
label: Operating System
|
||||||
description: What OS?
|
description: What OS?
|
||||||
options:
|
|
||||||
- Linux
|
|
||||||
- MacOS
|
|
||||||
- Windows
|
|
||||||
- Other (tell us in the description)
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
1
.github/workflows/pr-bandit.yml
vendored
1
.github/workflows/pr-bandit.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
config:
|
config:
|
||||||
|
- { 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}
|
- { python-version: "3.10", tox-env: security}
|
||||||
|
|||||||
4
.github/workflows/pr-python-pypy.yml
vendored
4
.github/workflows/pr-python-pypy.yml
vendored
@@ -5,11 +5,11 @@ on:
|
|||||||
tox-env:
|
tox-env:
|
||||||
description: "Tox Env to run on the PyPy Infra"
|
description: "Tox Env to run on the PyPy Infra"
|
||||||
required: false
|
required: false
|
||||||
default: "pypy310"
|
default: "pypy37"
|
||||||
pypy-version:
|
pypy-version:
|
||||||
description: "Version of PyPy to use"
|
description: "Version of PyPy to use"
|
||||||
required: false
|
required: false
|
||||||
default: "pypy-3.10"
|
default: "pypy-3.7"
|
||||||
jobs:
|
jobs:
|
||||||
testPyPy:
|
testPyPy:
|
||||||
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
|
|||||||
36
.github/workflows/pr-python37.yml
vendored
Normal file
36
.github/workflows/pr-python37.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Python 3.7 Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- current-release
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testPy37:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
# os: [ubuntu-latest, macos-latest]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- { python-version: 3.7, tox-env: py37 }
|
||||||
|
- { python-version: 3.7, tox-env: py37-no-ext }
|
||||||
|
steps:
|
||||||
|
- name: Checkout the Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }}"
|
||||||
|
test-failure-retry: "3"
|
||||||
1
.github/workflows/pr-type-check.yml
vendored
1
.github/workflows/pr-type-check.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
config:
|
config:
|
||||||
|
# - { 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}
|
- { python-version: "3.10", tox-env: type-checking}
|
||||||
|
|||||||
2
.github/workflows/pr-windows.yml
vendored
2
.github/workflows/pr-windows.yml
vendored
@@ -16,10 +16,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
config:
|
config:
|
||||||
|
- { 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: "3.10", tox-env: py310-no-ext }
|
||||||
- { python-version: "3.11", tox-env: py310-no-ext }
|
- { python-version: "3.11", tox-env: py310-no-ext }
|
||||||
|
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
|
|||||||
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.8", "3.9", "3.10", "3.11"]
|
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
55
.github/workflows/publish-package.yml
vendored
55
.github/workflows/publish-package.yml
vendored
@@ -1,39 +1,28 @@
|
|||||||
name: Upload Python Package
|
name: Publish Artifacts
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
jobs:
|
||||||
build-n-publish:
|
publishPythonPackage:
|
||||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
name: Publishing Sanic Release Artifacts
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout Repository
|
||||||
- name: Set up Python
|
uses: actions/checkout@v2
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
- name: Publish Python Package
|
||||||
python-version: "3.x"
|
uses: harshanarayana/custom-actions@main
|
||||||
- name: Install pypa/build
|
with:
|
||||||
run: >-
|
python-version: ${{ matrix.python-version }}
|
||||||
python3 -m
|
package-infra-name: "twine"
|
||||||
pip install
|
pypi-user: __token__
|
||||||
build
|
pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }}
|
||||||
--user
|
action: "package-publish"
|
||||||
- name: Build a binary wheel and a source tarball
|
pypi-verify-metadata: "true"
|
||||||
run: >-
|
|
||||||
python3 -m
|
|
||||||
build
|
|
||||||
--sdist
|
|
||||||
--wheel
|
|
||||||
--outdir dist/
|
|
||||||
.
|
|
||||||
# - name: Publish distribution 📦 to Test PyPI
|
|
||||||
# uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
# with:
|
|
||||||
# password: ${{ secrets.SANIC_TEST_PYPI_API_TOKEN }}
|
|
||||||
# repository-url: https://test.pypi.org/legacy/
|
|
||||||
- name: Publish distribution 📦 to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
with:
|
|
||||||
password: ${{ secrets.SANIC_PYPI_API_TOKEN }}
|
|
||||||
|
|||||||
27
README.rst
27
README.rst
@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
|
|||||||
:stub-columns: 1
|
:stub-columns: 1
|
||||||
|
|
||||||
* - Build
|
* - Build
|
||||||
- | |Py310Test| |Py39Test| |Py38Test|
|
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
|
||||||
* - Docs
|
* - Docs
|
||||||
- | |UserGuide| |Documentation|
|
- | |UserGuide| |Documentation|
|
||||||
* - Package
|
* - Package
|
||||||
@@ -19,7 +19,7 @@ Sanic | Build fast. Run fast.
|
|||||||
* - Support
|
* - Support
|
||||||
- | |Forums| |Discord| |Awesome|
|
- | |Forums| |Discord| |Awesome|
|
||||||
* - Stats
|
* - Stats
|
||||||
- | |Monthly Downloads| |Weekly Downloads| |Conda downloads|
|
- | |Downloads| |WkDownloads| |Conda downloads|
|
||||||
|
|
||||||
.. |UserGuide| image:: https://img.shields.io/badge/user%20guide-sanic-ff0068
|
.. |UserGuide| image:: https://img.shields.io/badge/user%20guide-sanic-ff0068
|
||||||
:target: https://sanicframework.org/
|
:target: https://sanicframework.org/
|
||||||
@@ -33,6 +33,8 @@ Sanic | Build fast. Run fast.
|
|||||||
: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
|
||||||
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml
|
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml
|
||||||
|
.. |Py37Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml/badge.svg?branch=main
|
||||||
|
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml
|
||||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||||
@@ -50,23 +52,19 @@ Sanic | Build fast. Run fast.
|
|||||||
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
|
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
|
||||||
:alt: Awesome Sanic List
|
:alt: Awesome Sanic List
|
||||||
:target: https://github.com/mekicha/awesome-sanic
|
:target: https://github.com/mekicha/awesome-sanic
|
||||||
.. |Monthly Downloads| image:: https://img.shields.io/pypi/dm/sanic.svg
|
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
|
||||||
:alt: Downloads
|
:alt: Downloads
|
||||||
:target: https://pepy.tech/project/sanic
|
:target: https://pepy.tech/project/sanic
|
||||||
.. |Weekly Downloads| image:: https://img.shields.io/pypi/dw/sanic.svg
|
.. |WkDownloads| image:: https://pepy.tech/badge/sanic/week
|
||||||
:alt: Downloads
|
:alt: Downloads
|
||||||
:target: https://pepy.tech/project/sanic
|
:target: https://pepy.tech/project/sanic
|
||||||
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
|
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
|
||||||
:alt: Downloads
|
:alt: Downloads
|
||||||
:target: https://anaconda.org/conda-forge/sanic
|
:target: https://anaconda.org/conda-forge/sanic
|
||||||
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
|
|
||||||
:alt: Linode
|
|
||||||
:target: https://www.linode.com
|
|
||||||
:width: 200px
|
|
||||||
|
|
||||||
.. end-badges
|
.. end-badges
|
||||||
|
|
||||||
Sanic is a **Python 3.8+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||||
|
|
||||||
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_.
|
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_.
|
||||||
|
|
||||||
@@ -143,17 +141,17 @@ And, we can verify it is working: ``curl localhost:8000 -i``
|
|||||||
|
|
||||||
**Now, let's go build something fast!**
|
**Now, let's go build something fast!**
|
||||||
|
|
||||||
Minimum Python version is 3.8. If you need Python 3.7 support, please use v22.12LTS.
|
Minimum Python version is 3.7. If you need Python 3.6 support, please use v20.12LTS.
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
`User Guide <https://sanic.dev>`__ and `API Documentation <http://sanic.readthedocs.io/>`__.
|
`User Guide <https://sanicframework.org>`__ and `API Documentation <http://sanic.readthedocs.io/>`__.
|
||||||
|
|
||||||
Changelog
|
Changelog
|
||||||
---------
|
---------
|
||||||
|
|
||||||
`Release Changelogs <https://sanic.readthedocs.io/en/stable/sanic/changelog.html>`__.
|
`Release Changelogs <https://github.com/sanic-org/sanic/blob/master/CHANGELOG.rst>`__.
|
||||||
|
|
||||||
|
|
||||||
Questions and Discussion
|
Questions and Discussion
|
||||||
@@ -165,3 +163,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
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
| 🔷 In support release
|
| 🔷 In support release
|
||||||
|
|
|
|
||||||
|
|
||||||
.. mdinclude:: ./releases/23/23.6.md
|
|
||||||
.. mdinclude:: ./releases/23/23.3.md
|
.. mdinclude:: ./releases/23/23.3.md
|
||||||
.. mdinclude:: ./releases/22/22.12.md
|
.. mdinclude:: ./releases/22/22.12.md
|
||||||
.. mdinclude:: ./releases/22/22.9.md
|
.. mdinclude:: ./releases/22/22.9.md
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## Version 23.3.0
|
## Version 23.3.0 🔶
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- [#2545](https://github.com/sanic-org/sanic/pull/2545) Standardize init of exceptions for more consistent control of HTTP responses using exceptions
|
- [#2545](https://github.com/sanic-org/sanic/pull/2545) Standardize init of exceptions for more consistent control of HTTP responses using exceptions
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
## Version 23.6.0 🔶
|
|
||||||
|
|
||||||
### Features
|
|
||||||
- [#2670](https://github.com/sanic-org/sanic/pull/2670) Increase `KEEP_ALIVE_TIMEOUT` default to 120 seconds
|
|
||||||
- [#2716](https://github.com/sanic-org/sanic/pull/2716) Adding allow route overwrite option in blueprint
|
|
||||||
- [#2724](https://github.com/sanic-org/sanic/pull/2724) and [#2792](https://github.com/sanic-org/sanic/pull/2792) Add a new exception signal for ALL exceptions raised anywhere in application
|
|
||||||
- [#2727](https://github.com/sanic-org/sanic/pull/2727) Add name prefixing to BP groups
|
|
||||||
- [#2754](https://github.com/sanic-org/sanic/pull/2754) Update request type on middleware types
|
|
||||||
- [#2770](https://github.com/sanic-org/sanic/pull/2770) Better exception message on startup time application induced import error
|
|
||||||
- [#2776](https://github.com/sanic-org/sanic/pull/2776) Set multiprocessing start method early
|
|
||||||
- [#2785](https://github.com/sanic-org/sanic/pull/2785) Add custom typing to config and ctx objects
|
|
||||||
- [#2790](https://github.com/sanic-org/sanic/pull/2790) Add `request.client_ip`
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- [#2728](https://github.com/sanic-org/sanic/pull/2728) Fix traversals for intended results
|
|
||||||
- [#2729](https://github.com/sanic-org/sanic/pull/2729) Handle case when headers argument of ResponseStream constructor is None
|
|
||||||
- [#2737](https://github.com/sanic-org/sanic/pull/2737) Fix type annotation for `JSONREsponse` default content type
|
|
||||||
- [#2740](https://github.com/sanic-org/sanic/pull/2740) Use Sanic's serializer for JSON responses in the Inspector
|
|
||||||
- [#2760](https://github.com/sanic-org/sanic/pull/2760) Support for `Request.get_current` in ASGI mode
|
|
||||||
- [#2773](https://github.com/sanic-org/sanic/pull/2773) Alow Blueprint routes to explicitly define error_format
|
|
||||||
- [#2774](https://github.com/sanic-org/sanic/pull/2774) Resolve headers on different renderers
|
|
||||||
- [#2782](https://github.com/sanic-org/sanic/pull/2782) Resolve pypy compatibility issues
|
|
||||||
|
|
||||||
### Deprecations and Removals
|
|
||||||
- [#2777](https://github.com/sanic-org/sanic/pull/2777) Remove Python 3.7 support
|
|
||||||
|
|
||||||
### Developer infrastructure
|
|
||||||
- [#2766](https://github.com/sanic-org/sanic/pull/2766) Unpin setuptools version
|
|
||||||
- [#2779](https://github.com/sanic-org/sanic/pull/2779) Run keep alive tests in loop to get available port
|
|
||||||
|
|
||||||
### Improved Documentation
|
|
||||||
- [#2741](https://github.com/sanic-org/sanic/pull/2741) Better documentation examples about running Sanic
|
|
||||||
From that list, the items to highlight in the release notes:
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
|
|
||||||
from sanic.__version__ import __version__
|
from sanic.__version__ import __version__
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.config import Config
|
|
||||||
from sanic.constants import HTTPMethod
|
from sanic.constants import HTTPMethod
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
BadRequest,
|
BadRequest,
|
||||||
@@ -37,29 +32,15 @@ from sanic.response import (
|
|||||||
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
||||||
|
|
||||||
|
|
||||||
DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]"
|
|
||||||
"""
|
|
||||||
A type alias for a Sanic app with a default config and namespace.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DefaultRequest: TypeAlias = Request[DefaultSanic, SimpleNamespace]
|
|
||||||
"""
|
|
||||||
A type alias for a request with a default Sanic app and namespace.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"__version__",
|
"__version__",
|
||||||
# Common objects
|
# Common objects
|
||||||
"Sanic",
|
"Sanic",
|
||||||
"Config",
|
|
||||||
"Blueprint",
|
"Blueprint",
|
||||||
"HTTPMethod",
|
"HTTPMethod",
|
||||||
"HTTPResponse",
|
"HTTPResponse",
|
||||||
"Request",
|
"Request",
|
||||||
"Websocket",
|
"Websocket",
|
||||||
# Common types
|
|
||||||
"DefaultSanic",
|
|
||||||
"DefaultRequest",
|
|
||||||
# Common exceptions
|
# Common exceptions
|
||||||
"BadRequest",
|
"BadRequest",
|
||||||
"ExpectationFailed",
|
"ExpectationFailed",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "23.6.0"
|
__version__ = "23.3.1"
|
||||||
|
|||||||
215
sanic/app.py
215
sanic/app.py
@@ -16,7 +16,7 @@ from asyncio import (
|
|||||||
from asyncio.futures import Future
|
from asyncio.futures import Future
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
from functools import partial, wraps
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from os import environ
|
from os import environ
|
||||||
from socket import socket
|
from socket import socket
|
||||||
@@ -28,11 +28,9 @@ from typing import (
|
|||||||
AnyStr,
|
AnyStr,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
ClassVar,
|
|
||||||
Coroutine,
|
Coroutine,
|
||||||
Deque,
|
Deque,
|
||||||
Dict,
|
Dict,
|
||||||
Generic,
|
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
@@ -42,8 +40,6 @@ from typing import (
|
|||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
|
||||||
overload,
|
|
||||||
)
|
)
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
|
|
||||||
@@ -58,12 +54,7 @@ from sanic.blueprint_group import BlueprintGroup
|
|||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||||
from sanic.config import SANIC_PREFIX, Config
|
from sanic.config import SANIC_PREFIX, Config
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import BadRequest, SanicException, ServerError, URLBuildError
|
||||||
BadRequest,
|
|
||||||
SanicException,
|
|
||||||
ServerError,
|
|
||||||
URLBuildError,
|
|
||||||
)
|
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.helpers import Default, _default
|
from sanic.helpers import Default, _default
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
@@ -86,7 +77,7 @@ from sanic.request import Request
|
|||||||
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
|
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
|
||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
from sanic.server.websockets.impl import ConnectionClosed
|
from sanic.server.websockets.impl import ConnectionClosed
|
||||||
from sanic.signals import Event, Signal, SignalRouter
|
from sanic.signals import Signal, SignalRouter
|
||||||
from sanic.touchup import TouchUp, TouchUpMeta
|
from sanic.touchup import TouchUp, TouchUpMeta
|
||||||
from sanic.types.shared_ctx import SharedContext
|
from sanic.types.shared_ctx import SharedContext
|
||||||
from sanic.worker.inspector import Inspector
|
from sanic.worker.inspector import Inspector
|
||||||
@@ -104,17 +95,8 @@ if TYPE_CHECKING:
|
|||||||
if OS_IS_WINDOWS: # no cov
|
if OS_IS_WINDOWS: # no cov
|
||||||
enable_windows_color_support()
|
enable_windows_color_support()
|
||||||
|
|
||||||
ctx_type = TypeVar("ctx_type")
|
|
||||||
config_type = TypeVar("config_type", bound=Config)
|
|
||||||
|
|
||||||
|
class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||||
class Sanic(
|
|
||||||
Generic[config_type, ctx_type],
|
|
||||||
StaticHandleMixin,
|
|
||||||
BaseSanic,
|
|
||||||
StartupMixin,
|
|
||||||
metaclass=TouchUpMeta,
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
The main application instance
|
The main application instance
|
||||||
"""
|
"""
|
||||||
@@ -169,102 +151,14 @@ class Sanic(
|
|||||||
"websocket_tasks",
|
"websocket_tasks",
|
||||||
)
|
)
|
||||||
|
|
||||||
_app_registry: ClassVar[Dict[str, "Sanic"]] = {}
|
_app_registry: Dict[str, "Sanic"] = {}
|
||||||
test_mode: ClassVar[bool] = False
|
test_mode = False
|
||||||
|
|
||||||
@overload
|
|
||||||
def __init__(
|
|
||||||
self: Sanic[Config, SimpleNamespace],
|
|
||||||
name: str,
|
|
||||||
config: None = None,
|
|
||||||
ctx: None = None,
|
|
||||||
router: Optional[Router] = None,
|
|
||||||
signal_router: Optional[SignalRouter] = None,
|
|
||||||
error_handler: Optional[ErrorHandler] = None,
|
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
|
||||||
request_class: Optional[Type[Request]] = None,
|
|
||||||
strict_slashes: bool = False,
|
|
||||||
log_config: Optional[Dict[str, Any]] = None,
|
|
||||||
configure_logging: bool = True,
|
|
||||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
|
||||||
loads: Optional[Callable[..., Any]] = None,
|
|
||||||
inspector: bool = False,
|
|
||||||
inspector_class: Optional[Type[Inspector]] = None,
|
|
||||||
certloader_class: Optional[Type[CertLoader]] = None,
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __init__(
|
|
||||||
self: Sanic[config_type, SimpleNamespace],
|
|
||||||
name: str,
|
|
||||||
config: Optional[config_type] = None,
|
|
||||||
ctx: None = None,
|
|
||||||
router: Optional[Router] = None,
|
|
||||||
signal_router: Optional[SignalRouter] = None,
|
|
||||||
error_handler: Optional[ErrorHandler] = None,
|
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
|
||||||
request_class: Optional[Type[Request]] = None,
|
|
||||||
strict_slashes: bool = False,
|
|
||||||
log_config: Optional[Dict[str, Any]] = None,
|
|
||||||
configure_logging: bool = True,
|
|
||||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
|
||||||
loads: Optional[Callable[..., Any]] = None,
|
|
||||||
inspector: bool = False,
|
|
||||||
inspector_class: Optional[Type[Inspector]] = None,
|
|
||||||
certloader_class: Optional[Type[CertLoader]] = None,
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __init__(
|
|
||||||
self: Sanic[Config, ctx_type],
|
|
||||||
name: str,
|
|
||||||
config: None = None,
|
|
||||||
ctx: Optional[ctx_type] = None,
|
|
||||||
router: Optional[Router] = None,
|
|
||||||
signal_router: Optional[SignalRouter] = None,
|
|
||||||
error_handler: Optional[ErrorHandler] = None,
|
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
|
||||||
request_class: Optional[Type[Request]] = None,
|
|
||||||
strict_slashes: bool = False,
|
|
||||||
log_config: Optional[Dict[str, Any]] = None,
|
|
||||||
configure_logging: bool = True,
|
|
||||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
|
||||||
loads: Optional[Callable[..., Any]] = None,
|
|
||||||
inspector: bool = False,
|
|
||||||
inspector_class: Optional[Type[Inspector]] = None,
|
|
||||||
certloader_class: Optional[Type[CertLoader]] = None,
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def __init__(
|
|
||||||
self: Sanic[config_type, ctx_type],
|
|
||||||
name: str,
|
|
||||||
config: Optional[config_type] = None,
|
|
||||||
ctx: Optional[ctx_type] = None,
|
|
||||||
router: Optional[Router] = None,
|
|
||||||
signal_router: Optional[SignalRouter] = None,
|
|
||||||
error_handler: Optional[ErrorHandler] = None,
|
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
|
||||||
request_class: Optional[Type[Request]] = None,
|
|
||||||
strict_slashes: bool = False,
|
|
||||||
log_config: Optional[Dict[str, Any]] = None,
|
|
||||||
configure_logging: bool = True,
|
|
||||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
|
||||||
loads: Optional[Callable[..., Any]] = None,
|
|
||||||
inspector: bool = False,
|
|
||||||
inspector_class: Optional[Type[Inspector]] = None,
|
|
||||||
certloader_class: Optional[Type[CertLoader]] = None,
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: Optional[str] = None,
|
||||||
config: Optional[config_type] = None,
|
config: Optional[Config] = None,
|
||||||
ctx: Optional[ctx_type] = None,
|
ctx: Optional[Any] = None,
|
||||||
router: Optional[Router] = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: Optional[SignalRouter] = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: Optional[ErrorHandler] = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
@@ -292,9 +186,7 @@ class Sanic(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# First setup config
|
# First setup config
|
||||||
self.config: config_type = cast(
|
self.config: Config = config or Config(env_prefix=env_prefix)
|
||||||
config_type, config or Config(env_prefix=env_prefix)
|
|
||||||
)
|
|
||||||
if inspector:
|
if inspector:
|
||||||
self.config.INSPECTOR = inspector
|
self.config.INSPECTOR = inspector
|
||||||
|
|
||||||
@@ -318,7 +210,7 @@ class Sanic(
|
|||||||
certloader_class or CertLoader
|
certloader_class or CertLoader
|
||||||
)
|
)
|
||||||
self.configure_logging: bool = configure_logging
|
self.configure_logging: bool = configure_logging
|
||||||
self.ctx: ctx_type = cast(ctx_type, ctx or SimpleNamespace())
|
self.ctx: Any = ctx or SimpleNamespace()
|
||||||
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
||||||
self.inspector_class: Type[Inspector] = inspector_class or Inspector
|
self.inspector_class: Type[Inspector] = inspector_class or Inspector
|
||||||
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
||||||
@@ -603,19 +495,6 @@ class Sanic(
|
|||||||
raise NotFound("Could not find signal %s" % event)
|
raise NotFound("Could not find signal %s" % event)
|
||||||
return await wait_for(signal.ctx.event.wait(), timeout=timeout)
|
return await wait_for(signal.ctx.event.wait(), timeout=timeout)
|
||||||
|
|
||||||
def report_exception(
|
|
||||||
self, handler: Callable[[Sanic, Exception], Coroutine[Any, Any, None]]
|
|
||||||
):
|
|
||||||
@wraps(handler)
|
|
||||||
async def report(exception: Exception) -> None:
|
|
||||||
await handler(self, exception)
|
|
||||||
|
|
||||||
self.add_signal(
|
|
||||||
handler=report, event=Event.SERVER_EXCEPTION_REPORT.value
|
|
||||||
)
|
|
||||||
|
|
||||||
return report
|
|
||||||
|
|
||||||
def enable_websocket(self, enable=True):
|
def enable_websocket(self, enable=True):
|
||||||
"""Enable or disable the support for websocket.
|
"""Enable or disable the support for websocket.
|
||||||
|
|
||||||
@@ -887,12 +766,10 @@ class Sanic(
|
|||||||
:raises ServerError: response 500
|
:raises ServerError: response 500
|
||||||
"""
|
"""
|
||||||
response = None
|
response = None
|
||||||
if not getattr(exception, "__dispatched__", False):
|
await self.dispatch(
|
||||||
... # DO NOT REMOVE THIS LINE. IT IS NEEDED FOR TOUCHUP.
|
"server.lifecycle.exception",
|
||||||
await self.dispatch(
|
context={"exception": exception},
|
||||||
"server.exception.report",
|
)
|
||||||
context={"exception": exception},
|
|
||||||
)
|
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.exception",
|
"http.lifecycle.exception",
|
||||||
inline=True,
|
inline=True,
|
||||||
@@ -1323,28 +1200,13 @@ class Sanic(
|
|||||||
app,
|
app,
|
||||||
loop,
|
loop,
|
||||||
):
|
):
|
||||||
async def do(task):
|
if callable(task):
|
||||||
try:
|
try:
|
||||||
if callable(task):
|
task = task(app)
|
||||||
try:
|
except TypeError:
|
||||||
task = task(app)
|
task = task()
|
||||||
except TypeError:
|
|
||||||
task = task()
|
|
||||||
if isawaitable(task):
|
|
||||||
await task
|
|
||||||
except CancelledError:
|
|
||||||
error_logger.warning(
|
|
||||||
f"Task {task} was cancelled before it completed."
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
await app.dispatch(
|
|
||||||
"server.exception.report",
|
|
||||||
context={"exception": e},
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
return do(task)
|
return task
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _loop_add_task(
|
def _loop_add_task(
|
||||||
@@ -1358,9 +1220,18 @@ class Sanic(
|
|||||||
) -> Task:
|
) -> Task:
|
||||||
if not isinstance(task, Future):
|
if not isinstance(task, Future):
|
||||||
prepped = cls._prep_task(task, app, loop)
|
prepped = cls._prep_task(task, app, loop)
|
||||||
task = loop.create_task(prepped, name=name)
|
if sys.version_info < (3, 8): # no cov
|
||||||
|
task = loop.create_task(prepped)
|
||||||
|
if name:
|
||||||
|
error_logger.warning(
|
||||||
|
"Cannot set a name for a task when using Python 3.7. "
|
||||||
|
"Your task will be created without a name."
|
||||||
|
)
|
||||||
|
task.get_name = lambda: name
|
||||||
|
else:
|
||||||
|
task = loop.create_task(prepped, name=name)
|
||||||
|
|
||||||
if name and register:
|
if name and register and sys.version_info > (3, 7):
|
||||||
app._task_registry[name] = task
|
app._task_registry[name] = task
|
||||||
|
|
||||||
return task
|
return task
|
||||||
@@ -1741,20 +1612,6 @@ class Sanic(
|
|||||||
if hasattr(self, "multiplexer"):
|
if hasattr(self, "multiplexer"):
|
||||||
self.multiplexer.ack()
|
self.multiplexer.ack()
|
||||||
|
|
||||||
def set_serving(self, serving: bool) -> None:
|
|
||||||
"""Set the serving state of the application.
|
|
||||||
|
|
||||||
This method is used to set the serving state of the application.
|
|
||||||
It is used internally by Sanic and should not typically be called
|
|
||||||
manually.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
serving (bool): Whether the application is serving.
|
|
||||||
"""
|
|
||||||
self.state.is_running = serving
|
|
||||||
if hasattr(self, "multiplexer"):
|
|
||||||
self.multiplexer.set_serving(serving)
|
|
||||||
|
|
||||||
async def _server_event(
|
async def _server_event(
|
||||||
self,
|
self,
|
||||||
concern: str,
|
concern: str,
|
||||||
@@ -1812,7 +1669,10 @@ class Sanic(
|
|||||||
def inspector(self):
|
def inspector(self):
|
||||||
if environ.get("SANIC_WORKER_PROCESS") or not self._inspector:
|
if environ.get("SANIC_WORKER_PROCESS") or not self._inspector:
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
"Can only access the inspector from the main process"
|
"Can only access the inspector from the main process "
|
||||||
|
"after main_process_start has run. For example, you most "
|
||||||
|
"likely want to use it inside the @app.main_process_ready "
|
||||||
|
"event listener."
|
||||||
)
|
)
|
||||||
return self._inspector
|
return self._inspector
|
||||||
|
|
||||||
@@ -1820,6 +1680,9 @@ class Sanic(
|
|||||||
def manager(self):
|
def manager(self):
|
||||||
if environ.get("SANIC_WORKER_PROCESS") or not self._manager:
|
if environ.get("SANIC_WORKER_PROCESS") or not self._manager:
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
"Can only access the manager from the main process"
|
"Can only access the manager from the main process "
|
||||||
|
"after main_process_start has run. For example, you most "
|
||||||
|
"likely want to use it inside the @app.main_process_ready "
|
||||||
|
"event listener."
|
||||||
)
|
)
|
||||||
return self._manager
|
return self._manager
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str = None,
|
||||||
url_prefix: Optional[str] = None,
|
url_prefix: Optional[str] = None,
|
||||||
host: Optional[Union[List[str], str]] = None,
|
host: Optional[Union[List[str], str]] = None,
|
||||||
version: Optional[Union[int, str, float]] = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
@@ -319,10 +319,6 @@ class Blueprint(BaseSanic):
|
|||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = self._setup_uri(future.uri, url_prefix)
|
uri = self._setup_uri(future.uri, url_prefix)
|
||||||
|
|
||||||
route_error_format = (
|
|
||||||
future.error_format if future.error_format else error_format
|
|
||||||
)
|
|
||||||
|
|
||||||
version_prefix = self.version_prefix
|
version_prefix = self.version_prefix
|
||||||
for prefix in (
|
for prefix in (
|
||||||
future.version_prefix,
|
future.version_prefix,
|
||||||
@@ -362,7 +358,7 @@ class Blueprint(BaseSanic):
|
|||||||
future.unquote,
|
future.unquote,
|
||||||
future.static,
|
future.static,
|
||||||
version_prefix,
|
version_prefix,
|
||||||
route_error_format,
|
error_format,
|
||||||
future.route_context,
|
future.route_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ DEFAULT_CONFIG = {
|
|||||||
"DEPRECATION_FILTER": "once",
|
"DEPRECATION_FILTER": "once",
|
||||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
"FORWARDED_SECRET": None,
|
"FORWARDED_SECRET": None,
|
||||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||||
"INSPECTOR": False,
|
"INSPECTOR": False,
|
||||||
"INSPECTOR_HOST": "localhost",
|
"INSPECTOR_HOST": "localhost",
|
||||||
"INSPECTOR_PORT": 6457,
|
"INSPECTOR_PORT": 6457,
|
||||||
"INSPECTOR_TLS_KEY": _default,
|
"INSPECTOR_TLS_KEY": _default,
|
||||||
"INSPECTOR_TLS_CERT": _default,
|
"INSPECTOR_TLS_CERT": _default,
|
||||||
"INSPECTOR_API_KEY": "",
|
"INSPECTOR_API_KEY": "",
|
||||||
"KEEP_ALIVE_TIMEOUT": 120,
|
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||||
"KEEP_ALIVE": True,
|
"KEEP_ALIVE": True,
|
||||||
"LOCAL_CERT_CREATOR": LocalCertCreator.AUTO,
|
"LOCAL_CERT_CREATOR": LocalCertCreator.AUTO,
|
||||||
"LOCAL_TLS_KEY": _default,
|
"LOCAL_TLS_KEY": _default,
|
||||||
@@ -61,16 +61,16 @@ DEFAULT_CONFIG = {
|
|||||||
"NOISY_EXCEPTIONS": False,
|
"NOISY_EXCEPTIONS": False,
|
||||||
"PROXIES_COUNT": None,
|
"PROXIES_COUNT": None,
|
||||||
"REAL_IP_HEADER": None,
|
"REAL_IP_HEADER": None,
|
||||||
"REQUEST_BUFFER_SIZE": 65536,
|
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
|
||||||
"REQUEST_MAX_HEADER_SIZE": 8192, # Cannot exceed 16384
|
"REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384
|
||||||
"REQUEST_ID_HEADER": "X-Request-ID",
|
"REQUEST_ID_HEADER": "X-Request-ID",
|
||||||
"REQUEST_MAX_SIZE": 100_000_000,
|
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||||
"REQUEST_TIMEOUT": 60,
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||||
"RESPONSE_TIMEOUT": 60,
|
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||||
"TLS_CERT_PASSWORD": "",
|
"TLS_CERT_PASSWORD": "",
|
||||||
"TOUCHUP": _default,
|
"TOUCHUP": _default,
|
||||||
"USE_UVLOOP": _default,
|
"USE_UVLOOP": _default,
|
||||||
"WEBSOCKET_MAX_SIZE": 2**20, # 1 MiB
|
"WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
|
||||||
"WEBSOCKET_PING_INTERVAL": 20,
|
"WEBSOCKET_PING_INTERVAL": 20,
|
||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ def exception_response(
|
|||||||
debug: bool,
|
debug: bool,
|
||||||
fallback: str,
|
fallback: str,
|
||||||
base: t.Type[BaseRenderer],
|
base: t.Type[BaseRenderer],
|
||||||
renderer: t.Optional[t.Type[BaseRenderer]] = None,
|
renderer: t.Type[t.Optional[BaseRenderer]] = None,
|
||||||
) -> HTTPResponse:
|
) -> HTTPResponse:
|
||||||
"""
|
"""
|
||||||
Render a response for the default FALLBACK exception handler.
|
Render a response for the default FALLBACK exception handler.
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class SanicException(Exception):
|
|||||||
|
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
self.status_code = status_code or self.status_code
|
self.status_code = status_code
|
||||||
self.quiet = quiet
|
self.quiet = quiet
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
|||||||
|
|
||||||
def parse_credentials(
|
def parse_credentials(
|
||||||
header: Optional[str],
|
header: Optional[str],
|
||||||
prefixes: Optional[Union[List, Tuple, Set]] = None,
|
prefixes: Union[List, Tuple, Set] = None,
|
||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""Parses any header with the aim to retrieve any credentials from it."""
|
"""Parses any header with the aim to retrieve any credentials from it."""
|
||||||
if not prefixes or not isinstance(prefixes, (list, tuple, set)):
|
if not prefixes or not isinstance(prefixes, (list, tuple, set)):
|
||||||
|
|||||||
@@ -38,15 +38,3 @@ class ExceptionMixin(metaclass=SanicMeta):
|
|||||||
return handler
|
return handler
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def all_exceptions(self, handler):
|
|
||||||
"""
|
|
||||||
This method enables the process of creating a global exception
|
|
||||||
handler for the current blueprint under question.
|
|
||||||
|
|
||||||
:param handler: A coroutine function to handle exceptions
|
|
||||||
|
|
||||||
:return a decorated method to handle global exceptions for any
|
|
||||||
route registered under this blueprint.
|
|
||||||
"""
|
|
||||||
return self.exception(Exception)(handler)
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, Optional, Set, Union
|
|||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
from sanic.models.futures import FutureSignal
|
from sanic.models.futures import FutureSignal
|
||||||
from sanic.models.handler_types import SignalHandler
|
from sanic.models.handler_types import SignalHandler
|
||||||
from sanic.signals import Event, Signal
|
from sanic.signals import Signal
|
||||||
from sanic.types import HashableDict
|
from sanic.types import HashableDict
|
||||||
|
|
||||||
|
|
||||||
@@ -80,9 +80,3 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
def event(self, event: str):
|
def event(self, event: str):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def catch_exception(self, handler):
|
|
||||||
async def signal_handler(exception: Exception):
|
|
||||||
await handler(self, exception)
|
|
||||||
|
|
||||||
self.signal(Event.SERVER_LIFECYCLE_EXCEPTION)(signal_handler)
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from asyncio import (
|
from asyncio import (
|
||||||
AbstractEventLoop,
|
AbstractEventLoop,
|
||||||
CancelledError,
|
CancelledError,
|
||||||
@@ -15,13 +16,7 @@ from asyncio import (
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from multiprocessing import (
|
from multiprocessing import Manager, Pipe, get_context
|
||||||
Manager,
|
|
||||||
Pipe,
|
|
||||||
get_context,
|
|
||||||
get_start_method,
|
|
||||||
set_start_method,
|
|
||||||
)
|
|
||||||
from multiprocessing.context import BaseContext
|
from multiprocessing.context import BaseContext
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socket import SHUT_RDWR, socket
|
from socket import SHUT_RDWR, socket
|
||||||
@@ -30,7 +25,6 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
ClassVar,
|
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
@@ -64,13 +58,13 @@ from sanic.server.protocols.http_protocol import HttpProtocol
|
|||||||
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
|
||||||
from sanic.server.runners import serve
|
from sanic.server.runners import serve
|
||||||
from sanic.server.socket import configure_socket, remove_unix_socket
|
from sanic.server.socket import configure_socket, remove_unix_socket
|
||||||
from sanic.worker.constants import ProcessState
|
|
||||||
from sanic.worker.loader import AppLoader
|
from sanic.worker.loader import AppLoader
|
||||||
from sanic.worker.manager import WorkerManager
|
from sanic.worker.manager import WorkerManager
|
||||||
from sanic.worker.multiplexer import WorkerMultiplexer
|
from sanic.worker.multiplexer import WorkerMultiplexer
|
||||||
from sanic.worker.reloader import Reloader
|
from sanic.worker.reloader import Reloader
|
||||||
from sanic.worker.serve import worker_serve
|
from sanic.worker.serve import worker_serve
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.application.state import ApplicationState
|
from sanic.application.state import ApplicationState
|
||||||
@@ -87,17 +81,13 @@ else: # no cov
|
|||||||
|
|
||||||
|
|
||||||
class StartupMixin(metaclass=SanicMeta):
|
class StartupMixin(metaclass=SanicMeta):
|
||||||
_app_registry: ClassVar[Dict[str, Sanic]]
|
_app_registry: Dict[str, Sanic]
|
||||||
|
|
||||||
config: Config
|
config: Config
|
||||||
listeners: Dict[str, List[ListenerType[Any]]]
|
listeners: Dict[str, List[ListenerType[Any]]]
|
||||||
state: ApplicationState
|
state: ApplicationState
|
||||||
websocket_enabled: bool
|
websocket_enabled: bool
|
||||||
multiplexer: WorkerMultiplexer
|
multiplexer: WorkerMultiplexer
|
||||||
|
start_method: StartMethod = _default
|
||||||
test_mode: ClassVar[bool]
|
|
||||||
start_method: ClassVar[StartMethod] = _default
|
|
||||||
START_METHOD_SET: ClassVar[bool] = False
|
|
||||||
|
|
||||||
def setup_loop(self):
|
def setup_loop(self):
|
||||||
if not self.asgi:
|
if not self.asgi:
|
||||||
@@ -701,26 +691,11 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
else "spawn"
|
else "spawn"
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _set_startup_method(cls) -> None:
|
|
||||||
if cls.START_METHOD_SET and not cls.test_mode:
|
|
||||||
return
|
|
||||||
|
|
||||||
method = cls._get_startup_method()
|
|
||||||
set_start_method(method, force=cls.test_mode)
|
|
||||||
cls.START_METHOD_SET = True
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_context(cls) -> BaseContext:
|
def _get_context(cls) -> BaseContext:
|
||||||
method = cls._get_startup_method()
|
method = cls._get_startup_method()
|
||||||
logger.debug("Creating multiprocessing context using '%s'", method)
|
logger.debug("Creating multiprocessing context using '%s'", method)
|
||||||
actual = get_start_method()
|
return get_context(method)
|
||||||
if method != actual:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Start method '{method}' was requested, but '{actual}' "
|
|
||||||
"was actually set."
|
|
||||||
)
|
|
||||||
return get_context()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serve(
|
def serve(
|
||||||
@@ -730,7 +705,6 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
app_loader: Optional[AppLoader] = None,
|
app_loader: Optional[AppLoader] = None,
|
||||||
factory: Optional[Callable[[], Sanic]] = None,
|
factory: Optional[Callable[[], Sanic]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
cls._set_startup_method()
|
|
||||||
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
||||||
apps = list(cls._app_registry.values())
|
apps = list(cls._app_registry.values())
|
||||||
if factory:
|
if factory:
|
||||||
@@ -892,6 +866,7 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
app.router.reset()
|
app.router.reset()
|
||||||
app.signal_router.reset()
|
app.signal_router.reset()
|
||||||
|
|
||||||
|
sync_manager.shutdown()
|
||||||
for sock in socks:
|
for sock in socks:
|
||||||
try:
|
try:
|
||||||
sock.shutdown(SHUT_RDWR)
|
sock.shutdown(SHUT_RDWR)
|
||||||
@@ -903,33 +878,12 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
loop.close()
|
loop.close()
|
||||||
cls._cleanup_env_vars()
|
cls._cleanup_env_vars()
|
||||||
cls._cleanup_apps()
|
cls._cleanup_apps()
|
||||||
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
limit = 100
|
|
||||||
while cls._get_process_states(worker_state):
|
|
||||||
sleep(0.1)
|
|
||||||
limit -= 1
|
|
||||||
if limit <= 0:
|
|
||||||
error_logger.warning(
|
|
||||||
"Worker shutdown timed out. "
|
|
||||||
"Some processes may still be running."
|
|
||||||
)
|
|
||||||
break
|
|
||||||
sync_manager.shutdown()
|
|
||||||
unix = kwargs.get("unix")
|
unix = kwargs.get("unix")
|
||||||
if unix:
|
if unix:
|
||||||
remove_unix_socket(unix)
|
remove_unix_socket(unix)
|
||||||
logger.info("Goodbye.")
|
|
||||||
if exit_code:
|
if exit_code:
|
||||||
os._exit(exit_code)
|
os._exit(exit_code)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_process_states(worker_state) -> List[str]:
|
|
||||||
return [
|
|
||||||
state for s in worker_state.values() if (state := s.get("state"))
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serve_single(cls, primary: Optional[Sanic] = None) -> None:
|
def serve_single(cls, primary: Optional[Sanic] = None) -> None:
|
||||||
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -15,10 +16,20 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
|
|||||||
|
|
||||||
class MockProtocol: # no cov
|
class MockProtocol: # no cov
|
||||||
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
|
||||||
self._not_paused = asyncio.Event()
|
# Fixup for 3.8+; Sanic still supports 3.7 where loop is required
|
||||||
self._not_paused.set()
|
loop = loop if sys.version_info[:2] < (3, 8) else None
|
||||||
self._complete = asyncio.Event()
|
# Optional in 3.9, necessary in 3.10 because the parameter "loop"
|
||||||
|
# was completely removed
|
||||||
|
if not loop:
|
||||||
|
self._not_paused = asyncio.Event()
|
||||||
|
self._not_paused.set()
|
||||||
|
self._complete = asyncio.Event()
|
||||||
|
else:
|
||||||
|
self._not_paused = asyncio.Event(loop=loop)
|
||||||
|
self._not_paused.set()
|
||||||
|
self._complete = asyncio.Event(loop=loop)
|
||||||
|
|
||||||
def pause_writing(self) -> None:
|
def pause_writing(self) -> None:
|
||||||
self._not_paused.clear()
|
self._not_paused.clear()
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from types import SimpleNamespace
|
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
DefaultDict,
|
DefaultDict,
|
||||||
Dict,
|
Dict,
|
||||||
Generic,
|
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Tuple,
|
Tuple,
|
||||||
@@ -17,7 +15,6 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from sanic_routing.route import Route
|
from sanic_routing.route import Route
|
||||||
from typing_extensions import TypeVar
|
|
||||||
|
|
||||||
from sanic.http.constants import HTTP # type: ignore
|
from sanic.http.constants import HTTP # type: ignore
|
||||||
from sanic.http.stream import Stream
|
from sanic.http.stream import Stream
|
||||||
@@ -26,13 +23,13 @@ from sanic.models.http_types import Credentials
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.app import Sanic
|
|
||||||
from sanic.config import Config
|
|
||||||
from sanic.server import ConnInfo
|
from sanic.server import ConnInfo
|
||||||
|
from sanic.app import Sanic
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from types import SimpleNamespace
|
||||||
from urllib.parse import parse_qs, parse_qsl, urlunparse
|
from urllib.parse import parse_qs, parse_qsl, urlunparse
|
||||||
|
|
||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
@@ -71,21 +68,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from json import loads as json_loads # type: ignore
|
from json import loads as json_loads # type: ignore
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
# The default argument of TypeVar is proposed to be added in Python 3.13
|
|
||||||
# by PEP 696 (https://www.python.org/dev/peps/pep-0696/).
|
|
||||||
# Therefore, we use typing_extensions.TypeVar for compatibility.
|
|
||||||
# For more information, see:
|
|
||||||
# https://discuss.python.org/t/pep-696-type-defaults-for-typevarlikes
|
|
||||||
sanic_type = TypeVar(
|
|
||||||
"sanic_type", bound=Sanic, default=Sanic[Config, SimpleNamespace]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
sanic_type = TypeVar("sanic_type")
|
|
||||||
ctx_type = TypeVar("ctx_type")
|
|
||||||
|
|
||||||
|
class Request:
|
||||||
class Request(Generic[sanic_type, ctx_type]):
|
|
||||||
"""
|
"""
|
||||||
Properties of an HTTP request such as URL, headers, etc.
|
Properties of an HTTP request such as URL, headers, etc.
|
||||||
"""
|
"""
|
||||||
@@ -96,7 +80,6 @@ class Request(Generic[sanic_type, ctx_type]):
|
|||||||
__slots__ = (
|
__slots__ = (
|
||||||
"__weakref__",
|
"__weakref__",
|
||||||
"_cookies",
|
"_cookies",
|
||||||
"_ctx",
|
|
||||||
"_id",
|
"_id",
|
||||||
"_ip",
|
"_ip",
|
||||||
"_parsed_url",
|
"_parsed_url",
|
||||||
@@ -113,6 +96,7 @@ class Request(Generic[sanic_type, ctx_type]):
|
|||||||
"app",
|
"app",
|
||||||
"body",
|
"body",
|
||||||
"conn_info",
|
"conn_info",
|
||||||
|
"ctx",
|
||||||
"head",
|
"head",
|
||||||
"headers",
|
"headers",
|
||||||
"method",
|
"method",
|
||||||
@@ -141,7 +125,7 @@ class Request(Generic[sanic_type, ctx_type]):
|
|||||||
version: str,
|
version: str,
|
||||||
method: str,
|
method: str,
|
||||||
transport: TransportProtocol,
|
transport: TransportProtocol,
|
||||||
app: sanic_type,
|
app: Sanic,
|
||||||
head: bytes = b"",
|
head: bytes = b"",
|
||||||
stream_id: int = 0,
|
stream_id: int = 0,
|
||||||
):
|
):
|
||||||
@@ -165,7 +149,7 @@ class Request(Generic[sanic_type, ctx_type]):
|
|||||||
# Init but do not inhale
|
# Init but do not inhale
|
||||||
self.body = b""
|
self.body = b""
|
||||||
self.conn_info: Optional[ConnInfo] = None
|
self.conn_info: Optional[ConnInfo] = None
|
||||||
self._ctx: Optional[ctx_type] = None
|
self.ctx = SimpleNamespace()
|
||||||
self.parsed_accept: Optional[AcceptList] = None
|
self.parsed_accept: Optional[AcceptList] = None
|
||||||
self.parsed_args: DefaultDict[
|
self.parsed_args: DefaultDict[
|
||||||
Tuple[bool, bool, str, str], RequestParameters
|
Tuple[bool, bool, str, str], RequestParameters
|
||||||
@@ -192,10 +176,6 @@ class Request(Generic[sanic_type, ctx_type]):
|
|||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
return f"<{class_name}: {self.method} {self.path}>"
|
return f"<{class_name}: {self.method} {self.path}>"
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def make_context() -> ctx_type:
|
|
||||||
return cast(ctx_type, SimpleNamespace())
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_current(cls) -> Request:
|
def get_current(cls) -> Request:
|
||||||
"""
|
"""
|
||||||
@@ -225,15 +205,6 @@ class Request(Generic[sanic_type, ctx_type]):
|
|||||||
def generate_id(*_):
|
def generate_id(*_):
|
||||||
return uuid.uuid4()
|
return uuid.uuid4()
|
||||||
|
|
||||||
@property
|
|
||||||
def ctx(self) -> ctx_type:
|
|
||||||
"""
|
|
||||||
:return: The current request context
|
|
||||||
"""
|
|
||||||
if not self._ctx:
|
|
||||||
self._ctx = self.make_context()
|
|
||||||
return self._ctx
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stream_id(self):
|
def stream_id(self):
|
||||||
"""
|
"""
|
||||||
@@ -838,31 +809,19 @@ class Request(Generic[sanic_type, ctx_type]):
|
|||||||
@property
|
@property
|
||||||
def remote_addr(self) -> str:
|
def remote_addr(self) -> str:
|
||||||
"""
|
"""
|
||||||
Client IP address, if available from proxy.
|
Client IP address, if available.
|
||||||
|
1. proxied remote address `self.forwarded['for']`
|
||||||
|
2. local remote address `self.ip`
|
||||||
|
|
||||||
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "_remote_addr"):
|
if not hasattr(self, "_remote_addr"):
|
||||||
self._remote_addr = str(self.forwarded.get("for", ""))
|
self._remote_addr = str(
|
||||||
|
self.forwarded.get("for", "")
|
||||||
|
) # or self.ip
|
||||||
return self._remote_addr
|
return self._remote_addr
|
||||||
|
|
||||||
@property
|
|
||||||
def client_ip(self) -> str:
|
|
||||||
"""
|
|
||||||
Client IP address.
|
|
||||||
1. proxied remote address `self.forwarded['for']`
|
|
||||||
2. local peer address `self.ip`
|
|
||||||
|
|
||||||
New in Sanic 23.6. Prefer this over `remote_addr` for determining the
|
|
||||||
client address regardless of whether the service runs behind a proxy
|
|
||||||
or not (proxy deployment needs separate configuration).
|
|
||||||
|
|
||||||
:return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
return self.remote_addr or self.ip
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scheme(self) -> str:
|
def scheme(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class Router(BaseRouter):
|
|||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
ignore_body: bool = False,
|
ignore_body: bool = False,
|
||||||
version: Optional[Union[str, float, int]] = None,
|
version: Union[str, float, int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
unquote: bool = False,
|
unquote: bool = False,
|
||||||
static: bool = False,
|
static: bool = False,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from ssl import SSLContext
|
from ssl import SSLContext
|
||||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
||||||
|
|
||||||
@@ -122,15 +124,17 @@ def _setup_system_signals(
|
|||||||
register_sys_signals: bool,
|
register_sys_signals: bool,
|
||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
) -> None: # no cov
|
) -> None: # no cov
|
||||||
signal_func(SIGINT, SIG_IGN)
|
# Ignore SIGINT when run_multiple
|
||||||
signal_func(SIGTERM, SIG_IGN)
|
if run_multiple:
|
||||||
os.environ["SANIC_WORKER_PROCESS"] = "true"
|
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:
|
||||||
if OS_IS_WINDOWS:
|
if OS_IS_WINDOWS:
|
||||||
ctrlc_workaround_for_windows(app)
|
ctrlc_workaround_for_windows(app)
|
||||||
else:
|
else:
|
||||||
for _signal in [SIGINT, SIGTERM]:
|
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(
|
||||||
_signal, partial(app.stop, terminate=False)
|
_signal, partial(app.stop, terminate=False)
|
||||||
)
|
)
|
||||||
@@ -141,6 +145,8 @@ def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
|||||||
try:
|
try:
|
||||||
server_logger.info("Starting worker [%s]", pid)
|
server_logger.info("Starting worker [%s]", pid)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
finally:
|
finally:
|
||||||
server_logger.info("Stopping worker [%s]", pid)
|
server_logger.info("Stopping worker [%s]", pid)
|
||||||
|
|
||||||
@@ -152,7 +158,6 @@ def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
|||||||
loop.run_until_complete(after_stop())
|
loop.run_until_complete(after_stop())
|
||||||
remove_unix_socket(unix)
|
remove_unix_socket(unix)
|
||||||
loop.close()
|
loop.close()
|
||||||
server_logger.info("Worker complete [%s]", pid)
|
|
||||||
|
|
||||||
|
|
||||||
def _serve_http_1(
|
def _serve_http_1(
|
||||||
@@ -246,7 +251,8 @@ def _serve_http_1(
|
|||||||
loop.run_until_complete(asyncio.sleep(0.1))
|
loop.run_until_complete(asyncio.sleep(0.1))
|
||||||
start_shutdown = start_shutdown + 0.1
|
start_shutdown = start_shutdown + 0.1
|
||||||
|
|
||||||
app.shutdown_tasks(graceful - start_shutdown)
|
if sys.version_info > (3, 7):
|
||||||
|
app.shutdown_tasks(graceful - start_shutdown)
|
||||||
|
|
||||||
# Force close non-idle connection after waiting for
|
# Force close non-idle connection after waiting for
|
||||||
# graceful_shutdown_timeout
|
# graceful_shutdown_timeout
|
||||||
@@ -256,11 +262,8 @@ def _serve_http_1(
|
|||||||
else:
|
else:
|
||||||
conn.abort()
|
conn.abort()
|
||||||
|
|
||||||
app.set_serving(False)
|
|
||||||
|
|
||||||
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
|
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
|
||||||
loop.run_until_complete(app._server_event("init", "after"))
|
loop.run_until_complete(app._server_event("init", "after"))
|
||||||
app.set_serving(True)
|
|
||||||
_run_server_forever(
|
_run_server_forever(
|
||||||
loop,
|
loop,
|
||||||
partial(app._server_event, "shutdown", "before"),
|
partial(app._server_event, "shutdown", "before"),
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ class WebsocketFrameAssembler:
|
|||||||
If ``timeout`` is set and elapses before a complete message is
|
If ``timeout`` is set and elapses before a complete message is
|
||||||
received, :meth:`get` returns ``None``.
|
received, :meth:`get` returns ``None``.
|
||||||
"""
|
"""
|
||||||
completed: bool
|
|
||||||
async with self.read_mutex:
|
async with self.read_mutex:
|
||||||
if timeout is not None and timeout <= 0:
|
if timeout is not None and timeout <= 0:
|
||||||
if not self.message_complete.is_set():
|
if not self.message_complete.is_set():
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from websockets.frames import Frame, Opcode
|
|||||||
|
|
||||||
|
|
||||||
try: # websockets < 11.0
|
try: # websockets < 11.0
|
||||||
from websockets.connection import Event, State # type: ignore
|
from websockets.connection import Event, State
|
||||||
from websockets.server import ServerConnection as ServerProtocol
|
from websockets.server import ServerConnection as ServerProtocol
|
||||||
except ImportError: # websockets >= 11.0
|
except ImportError: # websockets >= 11.0
|
||||||
from websockets.protocol import Event, State # type: ignore
|
from websockets.protocol import Event, State # type: ignore
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ from sanic.models.handler_types import SignalHandler
|
|||||||
|
|
||||||
|
|
||||||
class Event(Enum):
|
class Event(Enum):
|
||||||
SERVER_EXCEPTION_REPORT = "server.exception.report"
|
|
||||||
SERVER_INIT_AFTER = "server.init.after"
|
SERVER_INIT_AFTER = "server.init.after"
|
||||||
SERVER_INIT_BEFORE = "server.init.before"
|
SERVER_INIT_BEFORE = "server.init.before"
|
||||||
SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
|
SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
|
||||||
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
|
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
|
||||||
|
SERVER_LIFECYCLE_EXCEPTION = "server.lifecycle.exception"
|
||||||
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
|
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
|
||||||
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
|
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
|
||||||
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
|
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
|
||||||
@@ -40,11 +40,11 @@ class Event(Enum):
|
|||||||
|
|
||||||
RESERVED_NAMESPACES = {
|
RESERVED_NAMESPACES = {
|
||||||
"server": (
|
"server": (
|
||||||
Event.SERVER_EXCEPTION_REPORT.value,
|
|
||||||
Event.SERVER_INIT_AFTER.value,
|
Event.SERVER_INIT_AFTER.value,
|
||||||
Event.SERVER_INIT_BEFORE.value,
|
Event.SERVER_INIT_BEFORE.value,
|
||||||
Event.SERVER_SHUTDOWN_AFTER.value,
|
Event.SERVER_SHUTDOWN_AFTER.value,
|
||||||
Event.SERVER_SHUTDOWN_BEFORE.value,
|
Event.SERVER_SHUTDOWN_BEFORE.value,
|
||||||
|
Event.SERVER_LIFECYCLE_EXCEPTION.value,
|
||||||
),
|
),
|
||||||
"http": (
|
"http": (
|
||||||
Event.HTTP_LIFECYCLE_BEGIN.value,
|
Event.HTTP_LIFECYCLE_BEGIN.value,
|
||||||
@@ -174,12 +174,11 @@ class SignalRouter(BaseRouter):
|
|||||||
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
|
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
|
||||||
error_logger.exception(e)
|
error_logger.exception(e)
|
||||||
|
|
||||||
if event != Event.SERVER_EXCEPTION_REPORT.value:
|
if event != Event.SERVER_LIFECYCLE_EXCEPTION.value:
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
Event.SERVER_EXCEPTION_REPORT.value,
|
Event.SERVER_LIFECYCLE_EXCEPTION.value,
|
||||||
context={"exception": e},
|
context={"exception": e},
|
||||||
)
|
)
|
||||||
setattr(e, "__dispatched__", True)
|
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
for signal_event in events:
|
for signal_event in events:
|
||||||
@@ -230,6 +229,14 @@ class SignalRouter(BaseRouter):
|
|||||||
if not trigger:
|
if not trigger:
|
||||||
event = ".".join([*parts[:2], "<__trigger__>"])
|
event = ".".join([*parts[:2], "<__trigger__>"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attaching __requirements__ and __trigger__ to the handler
|
||||||
|
# is deprecated and will be removed in v23.6.
|
||||||
|
handler.__requirements__ = condition # type: ignore
|
||||||
|
handler.__trigger__ = trigger # type: ignore
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
signal = super().add(
|
signal = super().add(
|
||||||
event,
|
event,
|
||||||
handler,
|
handler,
|
||||||
|
|||||||
@@ -16,3 +16,5 @@ class ProcessState(IntEnum):
|
|||||||
ACKED = auto()
|
ACKED = auto()
|
||||||
JOINED = auto()
|
JOINED = auto()
|
||||||
TERMINATED = auto()
|
TERMINATED = auto()
|
||||||
|
FAILED = auto()
|
||||||
|
COMPLETED = auto()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from itertools import count
|
from enum import IntEnum, auto
|
||||||
|
from itertools import chain, count
|
||||||
from random import choice
|
from random import choice
|
||||||
from signal import SIGINT, SIGTERM, Signals
|
from signal import SIGINT, SIGTERM, Signals
|
||||||
from signal import signal as signal_func
|
from signal import signal as signal_func
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
from sanic.compat import OS_IS_WINDOWS
|
from sanic.compat import OS_IS_WINDOWS
|
||||||
from sanic.exceptions import ServerKilled
|
from sanic.exceptions import ServerKilled
|
||||||
@@ -13,13 +13,17 @@ from sanic.log import error_logger, logger
|
|||||||
from sanic.worker.constants import RestartOrder
|
from sanic.worker.constants import RestartOrder
|
||||||
from sanic.worker.process import ProcessState, Worker, WorkerProcess
|
from sanic.worker.process import ProcessState, Worker, WorkerProcess
|
||||||
|
|
||||||
|
|
||||||
if not OS_IS_WINDOWS:
|
if not OS_IS_WINDOWS:
|
||||||
from signal import SIGKILL
|
from signal import SIGKILL
|
||||||
else:
|
else:
|
||||||
SIGKILL = SIGINT
|
SIGKILL = SIGINT
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorCycle(IntEnum):
|
||||||
|
BREAK = auto()
|
||||||
|
CONTINUE = auto()
|
||||||
|
|
||||||
|
|
||||||
class WorkerManager:
|
class WorkerManager:
|
||||||
THRESHOLD = WorkerProcess.THRESHOLD
|
THRESHOLD = WorkerProcess.THRESHOLD
|
||||||
MAIN_IDENT = "Sanic-Main"
|
MAIN_IDENT = "Sanic-Main"
|
||||||
@@ -60,6 +64,8 @@ class WorkerManager:
|
|||||||
func: Callable[..., Any],
|
func: Callable[..., Any],
|
||||||
kwargs: Dict[str, Any],
|
kwargs: Dict[str, Any],
|
||||||
transient: bool = False,
|
transient: bool = False,
|
||||||
|
restartable: Optional[bool] = None,
|
||||||
|
tracked: bool = True,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
) -> Worker:
|
) -> Worker:
|
||||||
"""
|
"""
|
||||||
@@ -75,14 +81,35 @@ class WorkerManager:
|
|||||||
then the Worker Manager will restart the process along
|
then the Worker Manager will restart the process along
|
||||||
with any global restart (ex: auto-reload), defaults to False
|
with any global restart (ex: auto-reload), defaults to False
|
||||||
:type transient: bool, optional
|
:type transient: bool, optional
|
||||||
|
:param restartable: Whether to mark the process as restartable. If
|
||||||
|
True then the Worker Manager will be able to restart the process
|
||||||
|
if prompted. If transient=True, this property will be implied
|
||||||
|
to be True, defaults to None
|
||||||
|
:type restartable: Optional[bool], optional
|
||||||
|
:param tracked: Whether to track the process after completion,
|
||||||
|
defaults to True
|
||||||
:param workers: The number of worker processes to run, defaults to 1
|
:param workers: The number of worker processes to run, defaults to 1
|
||||||
:type workers: int, optional
|
:type workers: int, optional
|
||||||
:return: The Worker instance
|
:return: The Worker instance
|
||||||
:rtype: Worker
|
:rtype: Worker
|
||||||
"""
|
"""
|
||||||
|
if ident in self.transient or ident in self.durable:
|
||||||
|
raise ValueError(f"Worker {ident} already exists")
|
||||||
|
restartable = restartable if restartable is not None else transient
|
||||||
|
if transient and not restartable:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot create a transient worker that is not restartable"
|
||||||
|
)
|
||||||
container = self.transient if transient else self.durable
|
container = self.transient if transient else self.durable
|
||||||
worker = Worker(
|
worker = Worker(
|
||||||
ident, func, kwargs, self.context, self.worker_state, workers
|
ident,
|
||||||
|
func,
|
||||||
|
kwargs,
|
||||||
|
self.context,
|
||||||
|
self.worker_state,
|
||||||
|
workers,
|
||||||
|
restartable,
|
||||||
|
tracked,
|
||||||
)
|
)
|
||||||
container[worker.ident] = worker
|
container[worker.ident] = worker
|
||||||
return worker
|
return worker
|
||||||
@@ -94,6 +121,7 @@ class WorkerManager:
|
|||||||
self._serve,
|
self._serve,
|
||||||
self._server_settings,
|
self._server_settings,
|
||||||
transient=True,
|
transient=True,
|
||||||
|
restartable=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def shutdown_server(self, ident: Optional[str] = None) -> None:
|
def shutdown_server(self, ident: Optional[str] = None) -> None:
|
||||||
@@ -122,7 +150,6 @@ class WorkerManager:
|
|||||||
self.monitor()
|
self.monitor()
|
||||||
self.join()
|
self.join()
|
||||||
self.terminate()
|
self.terminate()
|
||||||
self.cleanup()
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
for process in self.processes:
|
for process in self.processes:
|
||||||
@@ -148,20 +175,38 @@ class WorkerManager:
|
|||||||
for process in self.processes:
|
for process in self.processes:
|
||||||
process.terminate()
|
process.terminate()
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""Cleanup the worker processes."""
|
|
||||||
for process in self.processes:
|
|
||||||
process.exit()
|
|
||||||
|
|
||||||
def restart(
|
def restart(
|
||||||
self,
|
self,
|
||||||
process_names: Optional[List[str]] = None,
|
process_names: Optional[List[str]] = None,
|
||||||
restart_order=RestartOrder.SHUTDOWN_FIRST,
|
restart_order=RestartOrder.SHUTDOWN_FIRST,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
restarted = set()
|
||||||
for process in self.transient_processes:
|
for process in self.transient_processes:
|
||||||
if not process_names or process.name in process_names:
|
if process.restartable and (
|
||||||
|
not process_names or process.name in process_names
|
||||||
|
):
|
||||||
process.restart(restart_order=restart_order, **kwargs)
|
process.restart(restart_order=restart_order, **kwargs)
|
||||||
|
restarted.add(process.name)
|
||||||
|
if process_names:
|
||||||
|
for process in self.durable_processes:
|
||||||
|
if process.restartable and process.name in process_names:
|
||||||
|
if process.state not in (
|
||||||
|
ProcessState.COMPLETED,
|
||||||
|
ProcessState.FAILED,
|
||||||
|
):
|
||||||
|
error_logger.error(
|
||||||
|
f"Cannot restart process {process.name} because "
|
||||||
|
"it is not in a final state. Current state is: "
|
||||||
|
f"{process.state.name}."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
process.restart(restart_order=restart_order, **kwargs)
|
||||||
|
restarted.add(process.name)
|
||||||
|
if process_names and not restarted:
|
||||||
|
error_logger.error(
|
||||||
|
f"Failed to restart processes: {', '.join(process_names)}"
|
||||||
|
)
|
||||||
|
|
||||||
def scale(self, num_worker: int):
|
def scale(self, num_worker: int):
|
||||||
if num_worker <= 0:
|
if num_worker <= 0:
|
||||||
@@ -189,45 +234,13 @@ class WorkerManager:
|
|||||||
self.wait_for_ack()
|
self.wait_for_ack()
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if self.monitor_subscriber.poll(0.1):
|
cycle = self._poll_monitor()
|
||||||
message = self.monitor_subscriber.recv()
|
if cycle is MonitorCycle.BREAK:
|
||||||
logger.debug(
|
break
|
||||||
f"Monitor message: {message}", extra={"verbosity": 2}
|
elif cycle is MonitorCycle.CONTINUE:
|
||||||
)
|
continue
|
||||||
if not message:
|
|
||||||
break
|
|
||||||
elif message == "__TERMINATE__":
|
|
||||||
self.shutdown()
|
|
||||||
break
|
|
||||||
logger.debug(
|
|
||||||
"Incoming monitor message: %s",
|
|
||||||
message,
|
|
||||||
extra={"verbosity": 1},
|
|
||||||
)
|
|
||||||
split_message = message.split(":", 2)
|
|
||||||
if message.startswith("__SCALE__"):
|
|
||||||
self.scale(int(split_message[-1]))
|
|
||||||
continue
|
|
||||||
processes = split_message[0]
|
|
||||||
reloaded_files = (
|
|
||||||
split_message[1] if len(split_message) > 1 else None
|
|
||||||
)
|
|
||||||
process_names = [
|
|
||||||
name.strip() for name in processes.split(",")
|
|
||||||
]
|
|
||||||
if "__ALL_PROCESSES__" in process_names:
|
|
||||||
process_names = None
|
|
||||||
order = (
|
|
||||||
RestartOrder.STARTUP_FIRST
|
|
||||||
if "STARTUP_FIRST" in split_message
|
|
||||||
else RestartOrder.SHUTDOWN_FIRST
|
|
||||||
)
|
|
||||||
self.restart(
|
|
||||||
process_names=process_names,
|
|
||||||
reloaded_files=reloaded_files,
|
|
||||||
restart_order=order,
|
|
||||||
)
|
|
||||||
self._sync_states()
|
self._sync_states()
|
||||||
|
self._cleanup_non_tracked_workers()
|
||||||
except InterruptedError:
|
except InterruptedError:
|
||||||
if not OS_IS_WINDOWS:
|
if not OS_IS_WINDOWS:
|
||||||
raise
|
raise
|
||||||
@@ -270,6 +283,10 @@ class WorkerManager:
|
|||||||
def workers(self) -> List[Worker]:
|
def workers(self) -> List[Worker]:
|
||||||
return list(self.transient.values()) + list(self.durable.values())
|
return list(self.transient.values()) + list(self.durable.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_workers(self) -> Iterable[Tuple[str, Worker]]:
|
||||||
|
return chain(self.transient.items(), self.durable.items())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def processes(self):
|
def processes(self):
|
||||||
for worker in self.workers:
|
for worker in self.workers:
|
||||||
@@ -282,6 +299,12 @@ class WorkerManager:
|
|||||||
for process in worker.processes:
|
for process in worker.processes:
|
||||||
yield process
|
yield process
|
||||||
|
|
||||||
|
@property
|
||||||
|
def durable_processes(self):
|
||||||
|
for worker in self.durable.values():
|
||||||
|
for process in worker.processes:
|
||||||
|
yield process
|
||||||
|
|
||||||
def kill(self):
|
def kill(self):
|
||||||
for process in self.processes:
|
for process in self.processes:
|
||||||
logger.info("Killing %s [%s]", process.name, process.pid)
|
logger.info("Killing %s [%s]", process.name, process.pid)
|
||||||
@@ -304,6 +327,25 @@ class WorkerManager:
|
|||||||
process.terminate()
|
process.terminate()
|
||||||
self._shutting_down = True
|
self._shutting_down = True
|
||||||
|
|
||||||
|
def remove_worker(self, worker: Worker) -> None:
|
||||||
|
if worker.tracked:
|
||||||
|
error_logger.error(
|
||||||
|
f"Worker {worker.ident} is tracked and cannot be removed."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if worker.has_alive_processes():
|
||||||
|
error_logger.error(
|
||||||
|
f"Worker {worker.ident} has alive processes and cannot be "
|
||||||
|
"removed."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.transient.pop(worker.ident, None)
|
||||||
|
self.durable.pop(worker.ident, None)
|
||||||
|
for process in worker.processes:
|
||||||
|
self.worker_state.pop(process.name, None)
|
||||||
|
logger.info("Removed worker %s", worker.ident)
|
||||||
|
del worker
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pid(self):
|
def pid(self):
|
||||||
return os.getpid()
|
return os.getpid()
|
||||||
@@ -323,5 +365,97 @@ class WorkerManager:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
process.set_state(ProcessState.TERMINATED, True)
|
process.set_state(ProcessState.TERMINATED, True)
|
||||||
continue
|
continue
|
||||||
|
if not process.is_alive():
|
||||||
|
state = "FAILED" if process.exitcode else "COMPLETED"
|
||||||
if state and process.state.name != state:
|
if state and process.state.name != state:
|
||||||
process.set_state(ProcessState[state], True)
|
process.set_state(ProcessState[state], True)
|
||||||
|
|
||||||
|
def _cleanup_non_tracked_workers(self) -> None:
|
||||||
|
to_remove = [
|
||||||
|
worker
|
||||||
|
for worker in self.workers
|
||||||
|
if not worker.tracked and not worker.has_alive_processes()
|
||||||
|
]
|
||||||
|
|
||||||
|
for worker in to_remove:
|
||||||
|
self.remove_worker(worker)
|
||||||
|
|
||||||
|
def _poll_monitor(self) -> Optional[MonitorCycle]:
|
||||||
|
if self.monitor_subscriber.poll(0.1):
|
||||||
|
message = self.monitor_subscriber.recv()
|
||||||
|
logger.debug(f"Monitor message: {message}", extra={"verbosity": 2})
|
||||||
|
if not message:
|
||||||
|
return MonitorCycle.BREAK
|
||||||
|
elif message == "__TERMINATE__":
|
||||||
|
self._handle_terminate()
|
||||||
|
return MonitorCycle.BREAK
|
||||||
|
elif isinstance(message, tuple) and len(message) == 7:
|
||||||
|
self._handle_manage(*message)
|
||||||
|
return MonitorCycle.CONTINUE
|
||||||
|
elif not isinstance(message, str):
|
||||||
|
error_logger.error(
|
||||||
|
"Monitor received an invalid message: %s", message
|
||||||
|
)
|
||||||
|
return MonitorCycle.CONTINUE
|
||||||
|
return self._handle_message(message)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_terminate(self) -> None:
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def _handle_message(self, message: str) -> Optional[MonitorCycle]:
|
||||||
|
logger.debug(
|
||||||
|
"Incoming monitor message: %s",
|
||||||
|
message,
|
||||||
|
extra={"verbosity": 1},
|
||||||
|
)
|
||||||
|
split_message = message.split(":", 2)
|
||||||
|
if message.startswith("__SCALE__"):
|
||||||
|
self.scale(int(split_message[-1]))
|
||||||
|
return MonitorCycle.CONTINUE
|
||||||
|
|
||||||
|
processes = split_message[0]
|
||||||
|
reloaded_files = split_message[1] if len(split_message) > 1 else None
|
||||||
|
process_names: Optional[List[str]] = [
|
||||||
|
name.strip() for name in processes.split(",")
|
||||||
|
]
|
||||||
|
if process_names and "__ALL_PROCESSES__" in process_names:
|
||||||
|
process_names = None
|
||||||
|
order = (
|
||||||
|
RestartOrder.STARTUP_FIRST
|
||||||
|
if "STARTUP_FIRST" in split_message
|
||||||
|
else RestartOrder.SHUTDOWN_FIRST
|
||||||
|
)
|
||||||
|
self.restart(
|
||||||
|
process_names=process_names,
|
||||||
|
reloaded_files=reloaded_files,
|
||||||
|
restart_order=order,
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_manage(
|
||||||
|
self,
|
||||||
|
ident: str,
|
||||||
|
func: Callable[..., Any],
|
||||||
|
kwargs: Dict[str, Any],
|
||||||
|
transient: bool,
|
||||||
|
restartable: Optional[bool],
|
||||||
|
tracked: bool,
|
||||||
|
workers: int,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
worker = self.manage(
|
||||||
|
ident,
|
||||||
|
func,
|
||||||
|
kwargs,
|
||||||
|
transient=transient,
|
||||||
|
restartable=restartable,
|
||||||
|
tracked=tracked,
|
||||||
|
workers=workers,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
error_logger.exception("Failed to manage worker %s", ident)
|
||||||
|
else:
|
||||||
|
for process in worker.processes:
|
||||||
|
process.start()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from multiprocessing.connection import Connection
|
from multiprocessing.connection import Connection
|
||||||
from os import environ, getpid
|
from os import environ, getpid
|
||||||
from typing import Any, Dict
|
from typing import Any, Callable, Dict, Optional
|
||||||
|
|
||||||
from sanic.log import Colors, logger
|
from sanic.log import Colors, logger
|
||||||
from sanic.worker.process import ProcessState
|
from sanic.worker.process import ProcessState
|
||||||
@@ -28,23 +28,26 @@ class WorkerMultiplexer:
|
|||||||
"state": ProcessState.ACKED.name,
|
"state": ProcessState.ACKED.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_serving(self, serving: bool) -> None:
|
def manage(
|
||||||
"""Set the worker to serving.
|
self,
|
||||||
|
ident: str,
|
||||||
Args:
|
func: Callable[..., Any],
|
||||||
serving (bool): Whether the worker is serving.
|
kwargs: Dict[str, Any],
|
||||||
"""
|
transient: bool = False,
|
||||||
self._state._state[self.name] = {
|
restartable: Optional[bool] = None,
|
||||||
**self._state._state[self.name],
|
tracked: bool = False,
|
||||||
"serving": serving,
|
workers: int = 1,
|
||||||
}
|
) -> None:
|
||||||
|
bundle = (
|
||||||
def exit(self):
|
ident,
|
||||||
"""Run cleanup at worker exit."""
|
func,
|
||||||
try:
|
kwargs,
|
||||||
del self._state._state[self.name]
|
transient,
|
||||||
except ConnectionRefusedError:
|
restartable,
|
||||||
logger.debug("Monitor process has already exited.")
|
tracked,
|
||||||
|
workers,
|
||||||
|
)
|
||||||
|
self._monitor_publisher.send(bundle)
|
||||||
|
|
||||||
def restart(
|
def restart(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from multiprocessing.context import BaseContext
|
from multiprocessing.context import BaseContext
|
||||||
from signal import SIGINT
|
from signal import SIGINT
|
||||||
@@ -20,13 +19,22 @@ class WorkerProcess:
|
|||||||
THRESHOLD = 300 # == 30 seconds
|
THRESHOLD = 300 # == 30 seconds
|
||||||
SERVER_LABEL = "Server"
|
SERVER_LABEL = "Server"
|
||||||
|
|
||||||
def __init__(self, factory, name, target, kwargs, worker_state):
|
def __init__(
|
||||||
|
self,
|
||||||
|
factory,
|
||||||
|
name,
|
||||||
|
target,
|
||||||
|
kwargs,
|
||||||
|
worker_state,
|
||||||
|
restartable: bool = False,
|
||||||
|
):
|
||||||
self.state = ProcessState.IDLE
|
self.state = ProcessState.IDLE
|
||||||
self.factory = factory
|
self.factory = factory
|
||||||
self.name = name
|
self.name = name
|
||||||
self.target = target
|
self.target = target
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.worker_state = worker_state
|
self.worker_state = worker_state
|
||||||
|
self.restartable = restartable
|
||||||
if self.name not in self.worker_state:
|
if self.name not in self.worker_state:
|
||||||
self.worker_state[self.name] = {
|
self.worker_state[self.name] = {
|
||||||
"server": self.SERVER_LABEL in self.name
|
"server": self.SERVER_LABEL in self.name
|
||||||
@@ -65,20 +73,6 @@ class WorkerProcess:
|
|||||||
self.set_state(ProcessState.JOINED)
|
self.set_state(ProcessState.JOINED)
|
||||||
self._current_process.join()
|
self._current_process.join()
|
||||||
|
|
||||||
def exit(self):
|
|
||||||
limit = 100
|
|
||||||
while self.is_alive() and limit > 0:
|
|
||||||
sleep(0.1)
|
|
||||||
limit -= 1
|
|
||||||
|
|
||||||
if not self.is_alive():
|
|
||||||
try:
|
|
||||||
del self.worker_state[self.name]
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
logger.debug("Monitor process has already exited.")
|
|
||||||
except KeyError:
|
|
||||||
logger.debug("Could not find worker state to delete.")
|
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
if self.state is not ProcessState.TERMINATED:
|
if self.state is not ProcessState.TERMINATED:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -91,6 +85,7 @@ class WorkerProcess:
|
|||||||
self.set_state(ProcessState.TERMINATED, force=True)
|
self.set_state(ProcessState.TERMINATED, force=True)
|
||||||
try:
|
try:
|
||||||
os.kill(self.pid, SIGINT)
|
os.kill(self.pid, SIGINT)
|
||||||
|
del self.worker_state[self.name]
|
||||||
except (KeyError, AttributeError, ProcessLookupError):
|
except (KeyError, AttributeError, ProcessLookupError):
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -131,16 +126,6 @@ class WorkerProcess:
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# def _run(self, **kwargs):
|
|
||||||
# atexit.register(self._exit)
|
|
||||||
# self.target(**kwargs)
|
|
||||||
|
|
||||||
# def _exit(self):
|
|
||||||
# try:
|
|
||||||
# del self.worker_state[self.name]
|
|
||||||
# except ConnectionRefusedError:
|
|
||||||
# logger.debug("Monitor process has already exited.")
|
|
||||||
|
|
||||||
def spawn(self):
|
def spawn(self):
|
||||||
if self.state not in (ProcessState.IDLE, ProcessState.RESTARTING):
|
if self.state not in (ProcessState.IDLE, ProcessState.RESTARTING):
|
||||||
raise Exception("Cannot spawn a worker process until it is idle.")
|
raise Exception("Cannot spawn a worker process until it is idle.")
|
||||||
@@ -155,6 +140,10 @@ class WorkerProcess:
|
|||||||
def pid(self):
|
def pid(self):
|
||||||
return self._current_process.pid
|
return self._current_process.pid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exitcode(self):
|
||||||
|
return self._current_process.exitcode
|
||||||
|
|
||||||
def _terminate_now(self):
|
def _terminate_now(self):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{Colors.BLUE}Begin restart termination: "
|
f"{Colors.BLUE}Begin restart termination: "
|
||||||
@@ -216,6 +205,8 @@ class Worker:
|
|||||||
context: BaseContext,
|
context: BaseContext,
|
||||||
worker_state: Dict[str, Any],
|
worker_state: Dict[str, Any],
|
||||||
num: int = 1,
|
num: int = 1,
|
||||||
|
restartable: bool = False,
|
||||||
|
tracked: bool = True,
|
||||||
):
|
):
|
||||||
self.ident = ident
|
self.ident = ident
|
||||||
self.num = num
|
self.num = num
|
||||||
@@ -224,6 +215,8 @@ class Worker:
|
|||||||
self.server_settings = server_settings
|
self.server_settings = server_settings
|
||||||
self.worker_state = worker_state
|
self.worker_state = worker_state
|
||||||
self.processes: Set[WorkerProcess] = set()
|
self.processes: Set[WorkerProcess] = set()
|
||||||
|
self.restartable = restartable
|
||||||
|
self.tracked = tracked
|
||||||
for _ in range(num):
|
for _ in range(num):
|
||||||
self.create_process()
|
self.create_process()
|
||||||
|
|
||||||
@@ -238,6 +231,10 @@ class Worker:
|
|||||||
target=self.serve,
|
target=self.serve,
|
||||||
kwargs={**self.server_settings},
|
kwargs={**self.server_settings},
|
||||||
worker_state=self.worker_state,
|
worker_state=self.worker_state,
|
||||||
|
restartable=self.restartable,
|
||||||
)
|
)
|
||||||
self.processes.add(process)
|
self.processes.add(process)
|
||||||
return process
|
return process
|
||||||
|
|
||||||
|
def has_alive_processes(self) -> bool:
|
||||||
|
return any(process.is_alive() for process in self.processes)
|
||||||
|
|||||||
11
setup.py
11
setup.py
@@ -83,11 +83,12 @@ setup_kwargs = {
|
|||||||
"packages": find_packages(exclude=("tests", "tests.*")),
|
"packages": find_packages(exclude=("tests", "tests.*")),
|
||||||
"package_data": {"sanic": ["py.typed", "pages/styles/*"]},
|
"package_data": {"sanic": ["py.typed", "pages/styles/*"]},
|
||||||
"platforms": "any",
|
"platforms": "any",
|
||||||
"python_requires": ">=3.8",
|
"python_requires": ">=3.7",
|
||||||
"classifiers": [
|
"classifiers": [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"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",
|
"Programming Language :: Python :: 3.10",
|
||||||
@@ -103,7 +104,7 @@ ujson = "ujson>=1.35" + env_dependency
|
|||||||
uvloop = "uvloop>=0.15.0" + env_dependency
|
uvloop = "uvloop>=0.15.0" + env_dependency
|
||||||
types_ujson = "types-ujson" + env_dependency
|
types_ujson = "types-ujson" + env_dependency
|
||||||
requirements = [
|
requirements = [
|
||||||
"sanic-routing>=23.6.0",
|
"sanic-routing>=22.8.0",
|
||||||
"httptools>=0.0.10",
|
"httptools>=0.0.10",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
@@ -112,11 +113,10 @@ requirements = [
|
|||||||
"multidict>=5.0,<7.0",
|
"multidict>=5.0,<7.0",
|
||||||
"html5tagger>=1.2.1",
|
"html5tagger>=1.2.1",
|
||||||
"tracerite>=1.0.0",
|
"tracerite>=1.0.0",
|
||||||
"typing-extensions>=4.4.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing>=23.6.0",
|
"sanic-testing>=23.3.0",
|
||||||
"pytest==7.1.*",
|
"pytest==7.1.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
@@ -127,7 +127,7 @@ tests_require = [
|
|||||||
"black",
|
"black",
|
||||||
"isort>=5.0.0",
|
"isort>=5.0.0",
|
||||||
"bandit",
|
"bandit",
|
||||||
"mypy",
|
"mypy>=0.901,<0.910",
|
||||||
"docutils",
|
"docutils",
|
||||||
"pygments",
|
"pygments",
|
||||||
"uvicorn<0.15.0",
|
"uvicorn<0.15.0",
|
||||||
@@ -143,7 +143,6 @@ docs_require = [
|
|||||||
"m2r2",
|
"m2r2",
|
||||||
"enum-tools[sphinx]",
|
"enum-tools[sphinx]",
|
||||||
"mistune<2.0.0",
|
"mistune<2.0.0",
|
||||||
"autodocsumm>=0.2.11",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
dev_require = tests_require + [
|
dev_require = tests_require + [
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ def test_handle_request_with_nested_sanic_exception(
|
|||||||
|
|
||||||
|
|
||||||
def test_app_name_required():
|
def test_app_name_required():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(SanicException):
|
||||||
Sanic()
|
Sanic()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import logging
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import sanic
|
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.config import Config
|
from sanic.config import Config
|
||||||
from sanic.errorpages import TextRenderer, exception_response, guess_mime
|
from sanic.errorpages import TextRenderer, exception_response, guess_mime
|
||||||
@@ -207,27 +205,6 @@ def test_route_error_response_from_explicit_format(app):
|
|||||||
assert response.content_type == "text/plain; charset=utf-8"
|
assert response.content_type == "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
def test_blueprint_error_response_from_explicit_format(app):
|
|
||||||
bp = sanic.Blueprint("MyBlueprint")
|
|
||||||
|
|
||||||
@bp.get("/text", error_format="json")
|
|
||||||
def text_response(request):
|
|
||||||
raise Exception("oops")
|
|
||||||
return text("Never gonna see this")
|
|
||||||
|
|
||||||
@bp.get("/json", error_format="text")
|
|
||||||
def json_response(request):
|
|
||||||
raise Exception("oops")
|
|
||||||
return json({"message": "Never gonna see this"})
|
|
||||||
|
|
||||||
app.blueprint(bp)
|
|
||||||
_, response = app.test_client.get("/text")
|
|
||||||
assert response.content_type == "application/json"
|
|
||||||
|
|
||||||
_, response = app.test_client.get("/json")
|
|
||||||
assert response.content_type == "text/plain; charset=utf-8"
|
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_fallback_format(app):
|
def test_unknown_fallback_format(app):
|
||||||
with pytest.raises(SanicException, match="Unknown format: bad"):
|
with pytest.raises(SanicException, match="Unknown format: bad"):
|
||||||
app.config.FALLBACK_ERROR_FORMAT = "bad"
|
app.config.FALLBACK_ERROR_FORMAT = "bad"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from sanic.response import text
|
|||||||
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
|
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
|
||||||
|
|
||||||
PORT = 42001 # test_keep_alive_timeout_reuse doesn't work with random port
|
PORT = 42001 # test_keep_alive_timeout_reuse doesn't work with random port
|
||||||
MAX_LOOPS = 15
|
|
||||||
port_counter = count()
|
port_counter = count()
|
||||||
|
|
||||||
|
|
||||||
@@ -70,35 +69,23 @@ def test_keep_alive_timeout_reuse():
|
|||||||
"""If the server keep-alive timeout and client keep-alive timeout are
|
"""If the server keep-alive timeout and client keep-alive timeout are
|
||||||
both longer than the delay, the client _and_ server will successfully
|
both longer than the delay, the client _and_ server will successfully
|
||||||
reuse the existing connection."""
|
reuse the existing connection."""
|
||||||
loops = 0
|
port = get_port()
|
||||||
while True:
|
loop = asyncio.new_event_loop()
|
||||||
port = get_port()
|
asyncio.set_event_loop(loop)
|
||||||
loop = asyncio.new_event_loop()
|
client = ReusableClient(keep_alive_timeout_app_reuse, loop=loop, port=port)
|
||||||
asyncio.set_event_loop(loop)
|
with client:
|
||||||
client = ReusableClient(
|
headers = {"Connection": "keep-alive"}
|
||||||
keep_alive_timeout_app_reuse, loop=loop, port=port
|
request, response = client.get("/1", headers=headers)
|
||||||
)
|
assert response.status == 200
|
||||||
try:
|
assert response.text == "OK"
|
||||||
with client:
|
assert request.protocol.state["requests_count"] == 1
|
||||||
headers = {"Connection": "keep-alive"}
|
|
||||||
request, response = client.get("/1", headers=headers)
|
|
||||||
assert response.status == 200
|
|
||||||
assert response.text == "OK"
|
|
||||||
assert request.protocol.state["requests_count"] == 1
|
|
||||||
|
|
||||||
loop.run_until_complete(aio_sleep(1))
|
loop.run_until_complete(aio_sleep(1))
|
||||||
|
|
||||||
request, response = client.get("/1")
|
request, response = client.get("/1")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
assert request.protocol.state["requests_count"] == 2
|
assert request.protocol.state["requests_count"] == 2
|
||||||
except OSError:
|
|
||||||
loops += 1
|
|
||||||
if loops > MAX_LOOPS:
|
|
||||||
raise
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
@@ -110,35 +97,23 @@ def test_keep_alive_timeout_reuse():
|
|||||||
def test_keep_alive_client_timeout():
|
def test_keep_alive_client_timeout():
|
||||||
"""If the server keep-alive timeout is longer than the client
|
"""If the server keep-alive timeout is longer than the client
|
||||||
keep-alive timeout, client will try to create a new connection here."""
|
keep-alive timeout, client will try to create a new connection here."""
|
||||||
loops = 0
|
port = get_port()
|
||||||
while True:
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
asyncio.set_event_loop(loop)
|
||||||
port = get_port()
|
client = ReusableClient(
|
||||||
loop = asyncio.new_event_loop()
|
keep_alive_app_client_timeout, loop=loop, port=port
|
||||||
asyncio.set_event_loop(loop)
|
)
|
||||||
client = ReusableClient(
|
with client:
|
||||||
keep_alive_app_client_timeout, loop=loop, port=port
|
headers = {"Connection": "keep-alive"}
|
||||||
)
|
request, response = client.get("/1", headers=headers, timeout=1)
|
||||||
with client:
|
|
||||||
headers = {"Connection": "keep-alive"}
|
|
||||||
request, response = client.get(
|
|
||||||
"/1", headers=headers, timeout=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
assert request.protocol.state["requests_count"] == 1
|
assert request.protocol.state["requests_count"] == 1
|
||||||
|
|
||||||
loop.run_until_complete(aio_sleep(2))
|
loop.run_until_complete(aio_sleep(2))
|
||||||
request, response = client.get("/1", timeout=1)
|
request, response = client.get("/1", timeout=1)
|
||||||
assert request.protocol.state["requests_count"] == 1
|
assert request.protocol.state["requests_count"] == 1
|
||||||
except OSError:
|
|
||||||
loops += 1
|
|
||||||
if loops > MAX_LOOPS:
|
|
||||||
raise
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
@@ -150,36 +125,24 @@ def test_keep_alive_server_timeout():
|
|||||||
keep-alive timeout, the client will either a 'Connection reset' error
|
keep-alive timeout, the client will either a 'Connection reset' error
|
||||||
_or_ a new connection. Depending on how the event-loop handles the
|
_or_ a new connection. Depending on how the event-loop handles the
|
||||||
broken server connection."""
|
broken server connection."""
|
||||||
loops = 0
|
port = get_port()
|
||||||
while True:
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
asyncio.set_event_loop(loop)
|
||||||
port = get_port()
|
client = ReusableClient(
|
||||||
loop = asyncio.new_event_loop()
|
keep_alive_app_server_timeout, loop=loop, port=port
|
||||||
asyncio.set_event_loop(loop)
|
)
|
||||||
client = ReusableClient(
|
with client:
|
||||||
keep_alive_app_server_timeout, loop=loop, port=port
|
headers = {"Connection": "keep-alive"}
|
||||||
)
|
request, response = client.get("/1", headers=headers, timeout=60)
|
||||||
with client:
|
|
||||||
headers = {"Connection": "keep-alive"}
|
|
||||||
request, response = client.get(
|
|
||||||
"/1", headers=headers, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
assert request.protocol.state["requests_count"] == 1
|
assert request.protocol.state["requests_count"] == 1
|
||||||
|
|
||||||
loop.run_until_complete(aio_sleep(3))
|
loop.run_until_complete(aio_sleep(3))
|
||||||
request, response = client.get("/1", timeout=60)
|
request, response = client.get("/1", timeout=60)
|
||||||
|
|
||||||
assert request.protocol.state["requests_count"] == 1
|
assert request.protocol.state["requests_count"] == 1
|
||||||
except OSError:
|
|
||||||
loops += 1
|
|
||||||
if loops > MAX_LOOPS:
|
|
||||||
raise
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
@@ -187,34 +150,20 @@ def test_keep_alive_server_timeout():
|
|||||||
reason="Not testable with current client",
|
reason="Not testable with current client",
|
||||||
)
|
)
|
||||||
def test_keep_alive_connection_context():
|
def test_keep_alive_connection_context():
|
||||||
loops = 0
|
port = get_port()
|
||||||
while True:
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
asyncio.set_event_loop(loop)
|
||||||
port = get_port()
|
client = ReusableClient(keep_alive_app_context, loop=loop, port=port)
|
||||||
loop = asyncio.new_event_loop()
|
with client:
|
||||||
asyncio.set_event_loop(loop)
|
headers = {"Connection": "keep-alive"}
|
||||||
client = ReusableClient(
|
request1, _ = client.post("/ctx", headers=headers)
|
||||||
keep_alive_app_context, loop=loop, port=port
|
|
||||||
)
|
|
||||||
with client:
|
|
||||||
headers = {"Connection": "keep-alive"}
|
|
||||||
request1, _ = client.post("/ctx", headers=headers)
|
|
||||||
|
|
||||||
loop.run_until_complete(aio_sleep(1))
|
loop.run_until_complete(aio_sleep(1))
|
||||||
request2, response = client.get("/ctx")
|
request2, response = client.get("/ctx")
|
||||||
|
|
||||||
assert response.text == "hello"
|
assert response.text == "hello"
|
||||||
assert id(request1.conn_info.ctx) == id(request2.conn_info.ctx)
|
assert id(request1.conn_info.ctx) == id(request2.conn_info.ctx)
|
||||||
assert (
|
assert (
|
||||||
request1.conn_info.ctx.foo
|
request1.conn_info.ctx.foo == request2.conn_info.ctx.foo == "hello"
|
||||||
== request2.conn_info.ctx.foo
|
)
|
||||||
== "hello"
|
assert request2.protocol.state["requests_count"] == 2
|
||||||
)
|
|
||||||
assert request2.protocol.state["requests_count"] == 2
|
|
||||||
except OSError:
|
|
||||||
loops += 1
|
|
||||||
if loops > MAX_LOOPS:
|
|
||||||
raise
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ def test_redirect_with_params(app, test_str):
|
|||||||
|
|
||||||
@app.route("/api/v2/test/<test>/", unquote=True)
|
@app.route("/api/v2/test/<test>/", unquote=True)
|
||||||
async def target_handler(request, test):
|
async def target_handler(request, test):
|
||||||
assert test == quote(test_str)
|
assert test == test_str
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
_, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/")
|
_, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/")
|
||||||
|
|||||||
@@ -310,29 +310,3 @@ def test_request_idempotent(method, idempotent):
|
|||||||
def test_request_cacheable(method, cacheable):
|
def test_request_cacheable(method, cacheable):
|
||||||
request = Request(b"/", {}, None, method, None, None)
|
request = Request(b"/", {}, None, method, None, None)
|
||||||
assert request.is_cacheable is cacheable
|
assert request.is_cacheable is cacheable
|
||||||
|
|
||||||
|
|
||||||
def test_custom_ctx():
|
|
||||||
class CustomContext:
|
|
||||||
FOO = "foo"
|
|
||||||
|
|
||||||
class CustomRequest(Request[Sanic, CustomContext]):
|
|
||||||
@staticmethod
|
|
||||||
def make_context() -> CustomContext:
|
|
||||||
return CustomContext()
|
|
||||||
|
|
||||||
app = Sanic("Test", request_class=CustomRequest)
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def handler(request: CustomRequest):
|
|
||||||
return response.json(
|
|
||||||
[
|
|
||||||
isinstance(request, CustomRequest),
|
|
||||||
isinstance(request.ctx, CustomContext),
|
|
||||||
request.ctx.FOO,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
_, resp = app.test_client.get("/")
|
|
||||||
|
|
||||||
assert resp.json == [True, True, "foo"]
|
|
||||||
|
|||||||
@@ -513,7 +513,6 @@ def test_standard_forwarded(app):
|
|||||||
request, response = app.test_client.get("/", headers=headers)
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
assert response.json == {"for": "127.0.0.2", "proto": "ws"}
|
assert response.json == {"for": "127.0.0.2", "proto": "ws"}
|
||||||
assert request.remote_addr == "127.0.0.2"
|
assert request.remote_addr == "127.0.0.2"
|
||||||
assert request.client_ip == "127.0.0.2"
|
|
||||||
assert request.scheme == "ws"
|
assert request.scheme == "ws"
|
||||||
assert request.server_name == "local.site"
|
assert request.server_name == "local.site"
|
||||||
assert request.server_port == 80
|
assert request.server_port == 80
|
||||||
@@ -738,7 +737,6 @@ def test_remote_addr_with_two_proxies(app):
|
|||||||
headers = {"X-Forwarded-For": "127.0.1.1"}
|
headers = {"X-Forwarded-For": "127.0.1.1"}
|
||||||
request, response = app.test_client.get("/", headers=headers)
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
assert request.remote_addr == ""
|
assert request.remote_addr == ""
|
||||||
assert request.client_ip == "127.0.0.1"
|
|
||||||
assert response.body == b""
|
assert response.body == b""
|
||||||
|
|
||||||
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
|
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic_testing.testing import HOST, PORT
|
from sanic_testing.testing import HOST, PORT
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
@@ -160,7 +158,7 @@ def test_signal_server_lifecycle_exception(app: Sanic):
|
|||||||
async def hello_route(request):
|
async def hello_route(request):
|
||||||
return HTTPResponse()
|
return HTTPResponse()
|
||||||
|
|
||||||
@app.signal(Event.SERVER_EXCEPTION_REPORT)
|
@app.signal(Event.SERVER_LIFECYCLE_EXCEPTION)
|
||||||
async def test_signal(exception: Exception):
|
async def test_signal(exception: Exception):
|
||||||
nonlocal trigger
|
nonlocal trigger
|
||||||
trigger = exception
|
trigger = exception
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import asyncio
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from itertools import count
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -10,7 +9,6 @@ from sanic_routing.exceptions import NotFound
|
|||||||
|
|
||||||
from sanic import Blueprint, Sanic, empty
|
from sanic import Blueprint, Sanic, empty
|
||||||
from sanic.exceptions import InvalidSignal, SanicException
|
from sanic.exceptions import InvalidSignal, SanicException
|
||||||
from sanic.signals import Event
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_signal(app):
|
def test_add_signal(app):
|
||||||
@@ -429,114 +427,3 @@ def test_signal_reservation(app, event, expected):
|
|||||||
app.signal(event)(lambda: ...)
|
app.signal(event)(lambda: ...)
|
||||||
else:
|
else:
|
||||||
app.signal(event)(lambda: ...)
|
app.signal(event)(lambda: ...)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_report_exception(app: Sanic):
|
|
||||||
@app.report_exception
|
|
||||||
async def catch_any_exception(app: Sanic, exception: Exception):
|
|
||||||
...
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def handler(request):
|
|
||||||
1 / 0
|
|
||||||
|
|
||||||
app.signal_router.finalize()
|
|
||||||
|
|
||||||
registered_signal_handlers = [
|
|
||||||
handler
|
|
||||||
for handler, *_ in app.signal_router.get(
|
|
||||||
Event.SERVER_EXCEPTION_REPORT.value
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
assert catch_any_exception in registered_signal_handlers
|
|
||||||
|
|
||||||
|
|
||||||
def test_report_exception_runs(app: Sanic):
|
|
||||||
event = asyncio.Event()
|
|
||||||
|
|
||||||
@app.report_exception
|
|
||||||
async def catch_any_exception(app: Sanic, exception: Exception):
|
|
||||||
event.set()
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def handler(request):
|
|
||||||
1 / 0
|
|
||||||
|
|
||||||
app.test_client.get("/")
|
|
||||||
|
|
||||||
assert event.is_set()
|
|
||||||
|
|
||||||
|
|
||||||
def test_report_exception_runs_once_inline(app: Sanic):
|
|
||||||
event = asyncio.Event()
|
|
||||||
c = count()
|
|
||||||
|
|
||||||
@app.report_exception
|
|
||||||
async def catch_any_exception(app: Sanic, exception: Exception):
|
|
||||||
event.set()
|
|
||||||
next(c)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def handler(request):
|
|
||||||
...
|
|
||||||
|
|
||||||
@app.signal(Event.HTTP_ROUTING_AFTER.value)
|
|
||||||
async def after_routing(**_):
|
|
||||||
1 / 0
|
|
||||||
|
|
||||||
app.test_client.get("/")
|
|
||||||
|
|
||||||
assert event.is_set()
|
|
||||||
assert next(c) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_report_exception_runs_once_custom(app: Sanic):
|
|
||||||
event = asyncio.Event()
|
|
||||||
c = count()
|
|
||||||
|
|
||||||
@app.report_exception
|
|
||||||
async def catch_any_exception(app: Sanic, exception: Exception):
|
|
||||||
event.set()
|
|
||||||
next(c)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def handler(request):
|
|
||||||
await app.dispatch("one.two.three")
|
|
||||||
return empty()
|
|
||||||
|
|
||||||
@app.signal("one.two.three")
|
|
||||||
async def one_two_three(**_):
|
|
||||||
1 / 0
|
|
||||||
|
|
||||||
app.test_client.get("/")
|
|
||||||
|
|
||||||
assert event.is_set()
|
|
||||||
assert next(c) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_report_exception_runs_task(app: Sanic):
|
|
||||||
c = count()
|
|
||||||
|
|
||||||
async def task_1():
|
|
||||||
next(c)
|
|
||||||
|
|
||||||
async def task_2(app):
|
|
||||||
next(c)
|
|
||||||
|
|
||||||
@app.report_exception
|
|
||||||
async def catch_any_exception(app: Sanic, exception: Exception):
|
|
||||||
next(c)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def handler(request):
|
|
||||||
app.add_task(task_1)
|
|
||||||
app.add_task(task_1())
|
|
||||||
app.add_task(task_2)
|
|
||||||
app.add_task(task_2(app))
|
|
||||||
return empty()
|
|
||||||
|
|
||||||
app.test_client.get("/")
|
|
||||||
|
|
||||||
assert next(c) == 4
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
from sanic import Sanic
|
|
||||||
from sanic.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(Config):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("test", config=CustomConfig())
|
|
||||||
reveal_type(app)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
|
|
||||||
class Foo:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("test", ctx=Foo())
|
|
||||||
reveal_type(app)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("test")
|
|
||||||
reveal_type(app)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from sanic import Sanic
|
|
||||||
from sanic.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(Config):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Foo:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("test", config=CustomConfig(), ctx=Foo())
|
|
||||||
reveal_type(app)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from sanic import Request, Sanic
|
|
||||||
from sanic.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class Foo:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("test")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def handler(request: Request[Sanic[Config, SimpleNamespace], Foo]):
|
|
||||||
reveal_type(request.ctx)
|
|
||||||
reveal_type(request.app)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from sanic import Request, Sanic
|
|
||||||
from sanic.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(Config):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("test", config=CustomConfig())
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def handler(
|
|
||||||
request: Request[Sanic[CustomConfig, SimpleNamespace], SimpleNamespace]
|
|
||||||
):
|
|
||||||
reveal_type(request.ctx)
|
|
||||||
reveal_type(request.app)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
from sanic import Request, Sanic
|
|
||||||
from sanic.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(Config):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Foo:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RequestContext:
|
|
||||||
foo: Foo
|
|
||||||
|
|
||||||
|
|
||||||
class CustomRequest(Request[Sanic[CustomConfig, Foo], RequestContext]):
|
|
||||||
@staticmethod
|
|
||||||
def make_context() -> RequestContext:
|
|
||||||
ctx = RequestContext()
|
|
||||||
ctx.foo = Foo()
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(
|
|
||||||
"test", config=CustomConfig(), ctx=Foo(), request_class=CustomRequest
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def handler(request: CustomRequest):
|
|
||||||
reveal_type(request)
|
|
||||||
reveal_type(request.ctx)
|
|
||||||
reveal_type(request.app)
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
# flake8: noqa: E501
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
CURRENT_DIR = Path(__file__).parent
|
|
||||||
|
|
||||||
|
|
||||||
def run_check(path_location: str) -> str:
|
|
||||||
"""Use mypy to check the given path location and return the output."""
|
|
||||||
|
|
||||||
mypy_path = "mypy"
|
|
||||||
path = CURRENT_DIR / path_location
|
|
||||||
command = [mypy_path, path.resolve().as_posix()]
|
|
||||||
|
|
||||||
process = subprocess.run(
|
|
||||||
command,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
universal_newlines=True,
|
|
||||||
)
|
|
||||||
output = process.stdout + process.stderr
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"path_location,expected",
|
|
||||||
(
|
|
||||||
(
|
|
||||||
"app_default.py",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]",
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"app_custom_config.py",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"sanic.app.Sanic[app_custom_config.CustomConfig, types.SimpleNamespace]",
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"app_custom_ctx.py",
|
|
||||||
[("sanic.app.Sanic[sanic.config.Config, app_custom_ctx.Foo]", 9)],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"app_fully_custom.py",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"sanic.app.Sanic[app_fully_custom.CustomConfig, app_fully_custom.Foo]",
|
|
||||||
14,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"request_custom_sanic.py",
|
|
||||||
[
|
|
||||||
("types.SimpleNamespace", 18),
|
|
||||||
(
|
|
||||||
"sanic.app.Sanic[request_custom_sanic.CustomConfig, types.SimpleNamespace]",
|
|
||||||
19,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"request_custom_ctx.py",
|
|
||||||
[
|
|
||||||
("request_custom_ctx.Foo", 16),
|
|
||||||
(
|
|
||||||
"sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]",
|
|
||||||
17,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"request_fully_custom.py",
|
|
||||||
[
|
|
||||||
("request_fully_custom.CustomRequest", 32),
|
|
||||||
("request_fully_custom.RequestContext", 33),
|
|
||||||
(
|
|
||||||
"sanic.app.Sanic[request_fully_custom.CustomConfig, request_fully_custom.Foo]",
|
|
||||||
34,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_check_app_default(
|
|
||||||
path_location: str, expected: List[Tuple[str, int]]
|
|
||||||
) -> None:
|
|
||||||
output = run_check(f"samples/{path_location}")
|
|
||||||
|
|
||||||
for text, number in expected:
|
|
||||||
current = CURRENT_DIR / f"samples/{path_location}"
|
|
||||||
path = current.relative_to(CURRENT_DIR.parent)
|
|
||||||
|
|
||||||
target = Path.cwd()
|
|
||||||
while True:
|
|
||||||
note = _text_from_path(current, path, target, number, text)
|
|
||||||
try:
|
|
||||||
assert note in output, output
|
|
||||||
except AssertionError:
|
|
||||||
target = target.parent
|
|
||||||
if not target.exists():
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def _text_from_path(
|
|
||||||
base: Path, path: Path, target: Path, number: int, text: str
|
|
||||||
) -> str:
|
|
||||||
relative_to_cwd = base.relative_to(target)
|
|
||||||
prefix = ".".join(relative_to_cwd.parts[:-1])
|
|
||||||
text = text.replace(path.stem, f"{prefix}.{path.stem}")
|
|
||||||
return f'{path}:{number}: note: Revealed type is "{text}"'
|
|
||||||
8
tox.ini
8
tox.ini
@@ -1,14 +1,14 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py38, py39, py310, py311, pyNightly, pypy310, {py38,py39,py310,py311,pyNightly,pypy310}-no-ext, lint, check, security, docs, type-checking
|
envlist = py37, py38, py39, py310, py311, pyNightly, pypy37, {py37,py38,py39,py310,py311,pyNightly,pypy37}-no-ext, lint, check, security, docs, type-checking
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = true
|
usedevelop = true
|
||||||
setenv =
|
setenv =
|
||||||
{py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UJSON=1
|
{py37,py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UJSON=1
|
||||||
{py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
|
{py37,py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
|
||||||
extras = test, http3
|
extras = test, http3
|
||||||
deps =
|
deps =
|
||||||
httpx>=0.23
|
httpx==0.23
|
||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
pytest
|
pytest
|
||||||
coverage
|
coverage
|
||||||
|
|||||||
Reference in New Issue
Block a user