Compare commits
154 Commits
v22.6.1
...
sml-change
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9f7086ee6 | ||
|
|
26e999dec0 | ||
|
|
8f305047c0 | ||
|
|
59f9b5cc28 | ||
|
|
94eff28fda | ||
|
|
c111d93840 | ||
|
|
3587a4fe15 | ||
|
|
51a9605668 | ||
|
|
14f16352fc | ||
|
|
ecf34896c8 | ||
|
|
e544cd8af6 | ||
|
|
aa8af0dcea | ||
|
|
db0b9046d7 | ||
|
|
f798eda446 | ||
|
|
21ad1ae61b | ||
|
|
d8bf65ad1b | ||
|
|
844cab2d6b | ||
|
|
b0cb01d1a4 | ||
|
|
4c55051442 | ||
|
|
37f3607ebc | ||
|
|
7e617c1769 | ||
|
|
a5f732cc80 | ||
|
|
ce19908bc0 | ||
|
|
a6efebda56 | ||
|
|
da9ff33fa7 | ||
|
|
526115c3c5 | ||
|
|
12ba685bf6 | ||
|
|
39b98e6b45 | ||
|
|
3ddbda61d9 | ||
|
|
68bf26df17 | ||
|
|
a773ad2354 | ||
|
|
47b2459811 | ||
|
|
7491d567a3 | ||
|
|
783a29bc0b | ||
|
|
cf76c05d3f | ||
|
|
d6f2613623 | ||
|
|
a6ff13ceed | ||
|
|
207f8af11f | ||
|
|
5c65118d12 | ||
|
|
f4792a2bc6 | ||
|
|
53b7a5a5a1 | ||
|
|
5e7f6998bd | ||
|
|
c7a71cd00c | ||
|
|
9cb9e88678 | ||
|
|
ebc2f46682 | ||
|
|
ff47448585 | ||
|
|
2df5b19fd4 | ||
|
|
5dfd48f855 | ||
|
|
ddf3a49988 | ||
|
|
1b43aa5f2f | ||
|
|
713abe3cf2 | ||
|
|
b46b81d43a | ||
|
|
30c53b6857 | ||
|
|
4f000ab59c | ||
|
|
ae757c8ad6 | ||
|
|
f30f53f67d | ||
|
|
ea09906e0a | ||
|
|
77bdfa14ed | ||
|
|
faf1ff8d4f | ||
|
|
b5175238fb | ||
|
|
32d62c2db4 | ||
|
|
a00ec8ab37 | ||
|
|
859a8130c1 | ||
|
|
2038799d7a | ||
|
|
41da8bbd61 | ||
|
|
e328d4406b | ||
|
|
d9c883eb9b | ||
|
|
10d4f2803a | ||
|
|
fa6dbddf69 | ||
|
|
2c8f1807d8 | ||
|
|
ca0e933813 | ||
|
|
2e36507a60 | ||
|
|
39a4a75dcb | ||
|
|
e8bb2834d6 | ||
|
|
36e3cc9df7 | ||
|
|
fed2ef3527 | ||
|
|
6673acf544 | ||
|
|
4ad8168bb0 | ||
|
|
28f5b3c301 | ||
|
|
c573019e7f | ||
|
|
029f564032 | ||
|
|
2abe66b670 | ||
|
|
911485d52e | ||
|
|
4744a89c33 | ||
|
|
f7040ccec8 | ||
|
|
518152d97e | ||
|
|
0e44e9cacb | ||
|
|
bfb54b0969 | ||
|
|
154863d6c6 | ||
|
|
a3ff0c13b7 | ||
|
|
95ee518aec | ||
|
|
71d3d87bcc | ||
|
|
b276b91c21 | ||
|
|
064168f3c8 | ||
|
|
db39e127bf | ||
|
|
13e9ab7ba9 | ||
|
|
92e7463721 | ||
|
|
8e720365c2 | ||
|
|
d4041161c7 | ||
|
|
f32437bf13 | ||
|
|
0909e94527 | ||
|
|
aef2673c38 | ||
|
|
4c14910d5b | ||
|
|
beae35f921 | ||
|
|
ad4e526c77 | ||
|
|
4422d0c34d | ||
|
|
ad9183d21d | ||
|
|
d70636ba2e | ||
|
|
da23f85675 | ||
|
|
3f4663b9f8 | ||
|
|
65d7447cf6 | ||
|
|
5369291c27 | ||
|
|
1c4925edf7 | ||
|
|
6b9edfd05c | ||
|
|
97f33f42df | ||
|
|
15a588a90c | ||
|
|
82421e7efc | ||
|
|
f891995b48 | ||
|
|
5052321801 | ||
|
|
23ce4eaaa4 | ||
|
|
23a430c4ad | ||
|
|
ec158ffa69 | ||
|
|
6e32270036 | ||
|
|
43ba381e7b | ||
|
|
16503319e5 | ||
|
|
389363ab71 | ||
|
|
7f894c45b3 | ||
|
|
4726cf1910 | ||
|
|
d352a4155e | ||
|
|
e5010286b4 | ||
|
|
358498db96 | ||
|
|
e4999401ab | ||
|
|
c8df0aa2cb | ||
|
|
5fb207176b | ||
|
|
a12b560478 | ||
|
|
753ee992a6 | ||
|
|
09089b1bd3 | ||
|
|
7ddbe5e844 | ||
|
|
ab5a7038af | ||
|
|
4f3c780dc3 | ||
|
|
71f7765a4c | ||
|
|
0392d1dcfc | ||
|
|
7827b1b41d | ||
|
|
8e9342e188 | ||
|
|
2f6f2bfa76 | ||
|
|
dee09d7fff | ||
|
|
9cf38a0a83 | ||
|
|
3def3d3569 | ||
|
|
e100a14fd4 | ||
|
|
2fa28f1711 | ||
|
|
9d415e4ec6 | ||
|
|
312ab298fd | ||
|
|
2fc21ad576 | ||
|
|
8f6c87c3d6 |
11
.coveragerc
11
.coveragerc
@@ -4,11 +4,12 @@ source = sanic
|
|||||||
omit =
|
omit =
|
||||||
site-packages
|
site-packages
|
||||||
sanic/__main__.py
|
sanic/__main__.py
|
||||||
|
sanic/server/legacy.py
|
||||||
sanic/compat.py
|
sanic/compat.py
|
||||||
sanic/reloader_helpers.py
|
|
||||||
sanic/simple.py
|
sanic/simple.py
|
||||||
sanic/utils.py
|
sanic/utils.py
|
||||||
sanic/cli
|
sanic/cli
|
||||||
|
sanic/pages
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
directory = coverage
|
directory = coverage
|
||||||
@@ -21,12 +22,4 @@ exclude_lines =
|
|||||||
NOQA
|
NOQA
|
||||||
pragma: no cover
|
pragma: no cover
|
||||||
TYPE_CHECKING
|
TYPE_CHECKING
|
||||||
omit =
|
|
||||||
site-packages
|
|
||||||
sanic/__main__.py
|
|
||||||
sanic/compat.py
|
|
||||||
sanic/reloader_helpers.py
|
|
||||||
sanic/simple.py
|
|
||||||
sanic/utils.py
|
|
||||||
sanic/cli
|
|
||||||
skip_empty = True
|
skip_empty = True
|
||||||
|
|||||||
66
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
66
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: ["bug", "triage"]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: existing
|
||||||
|
attributes:
|
||||||
|
label: Is there an existing issue for this?
|
||||||
|
description: Please search to see if an issue already exists for the bug you encountered.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks using markdown code-block syntax to make it easier to read.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: code
|
||||||
|
attributes:
|
||||||
|
label: Code snippet
|
||||||
|
description: Relevant source code, make sure to remove what is not necessary.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: A concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: running
|
||||||
|
attributes:
|
||||||
|
label: How do you run Sanic?
|
||||||
|
options:
|
||||||
|
- Sanic CLI
|
||||||
|
- As a module
|
||||||
|
- As a script (`app.run` or `Sanic.serve`)
|
||||||
|
- ASGI
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: What OS?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Sanic Version
|
||||||
|
description: Check startup logs or try `sanic --version`
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
|
|
||||||
|
|
||||||
|
|
||||||
**Code snippet**
|
|
||||||
Relevant source code, make sure to remove what is not necessary.
|
|
||||||
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
|
|
||||||
**Environment (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Version [e.g. 0.8.3]
|
|
||||||
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Questions and Help
|
- name: Questions and Help
|
||||||
url: https://community.sanicframework.org/c/questions-and-help
|
url: https://community.sanicframework.org/c/questions-and-help
|
||||||
about: Do you need help with Sanic? Ask your questions here.
|
about: Do you need help with Sanic? Ask your questions here.
|
||||||
|
- name: Discussion and Support
|
||||||
|
url: https://discord.gg/FARQzAEMAA
|
||||||
|
about: For live discussion and support, checkout the Sanic Discord server.
|
||||||
|
|||||||
34
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
34
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: 🌟 Feature request
|
||||||
|
description: Suggest an enhancement for Sanic
|
||||||
|
labels: ["feature request"]
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: existing
|
||||||
|
attributes:
|
||||||
|
label: Is there an existing issue for this?
|
||||||
|
description: Please search to see if an issue already exists for the enhancement you are proposing.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem? Please describe.
|
||||||
|
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: code
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for Sanic
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or sample code about the feature request here.
|
|
||||||
1
.github/workflows/pr-bandit.yml
vendored
1
.github/workflows/pr-bandit.yml
vendored
@@ -20,6 +20,7 @@ jobs:
|
|||||||
- { 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}
|
||||||
|
- { python-version: "3.11", tox-env: security}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
2
.github/workflows/pr-docs.yml
vendored
2
.github/workflows/pr-docs.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
config:
|
config:
|
||||||
- {python-version: "3.8", tox-env: "docs"}
|
- {python-version: "3.10", tox-env: "docs"}
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/pr-linter.yml
vendored
2
.github/workflows/pr-linter.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
config:
|
config:
|
||||||
- { python-version: 3.8, tox-env: lint}
|
- { python-version: "3.10", tox-env: lint}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
47
.github/workflows/pr-python311.yml
vendored
Normal file
47
.github/workflows/pr-python311.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Python 3.11 Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "*LTS"
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testPy311:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# os: [ubuntu-latest, macos-latest]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
config:
|
||||||
|
- {
|
||||||
|
python-version: "3.11",
|
||||||
|
tox-env: py311,
|
||||||
|
ignore-error-flake: "false",
|
||||||
|
command-timeout: "0",
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
python-version: "3.11",
|
||||||
|
tox-env: py311-no-ext,
|
||||||
|
ignore-error-flake: "true",
|
||||||
|
command-timeout: "600000",
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
id: checkout-branch
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
uses: harshanarayana/custom-actions@main
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.config.python-version }}
|
||||||
|
test-infra-tool: tox
|
||||||
|
test-infra-version: latest
|
||||||
|
action: tests
|
||||||
|
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
|
||||||
|
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
|
||||||
|
command-timeout: "${{ matrix.config.command-timeout }}"
|
||||||
|
test-failure-retry: "3"
|
||||||
1
.github/workflows/pr-type-check.yml
vendored
1
.github/workflows/pr-type-check.yml
vendored
@@ -20,6 +20,7 @@ jobs:
|
|||||||
- { 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}
|
||||||
|
- { python-version: "3.11", tox-env: type-checking}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
1
.github/workflows/pr-windows.yml
vendored
1
.github/workflows/pr-windows.yml
vendored
@@ -19,6 +19,7 @@ jobs:
|
|||||||
- { 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: pypy-3.7, tox-env: pypy37-no-ext }
|
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
2
.github/workflows/publish-images.yml
vendored
2
.github/workflows/publish-images.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
2
.github/workflows/publish-package.yml
vendored
2
.github/workflows/publish-package.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8"]
|
python-version: ["3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
|
|||||||
@@ -313,8 +313,8 @@ Version 21.3.0
|
|||||||
`#2074 <https://github.com/sanic-org/sanic/pull/2074>`_
|
`#2074 <https://github.com/sanic-org/sanic/pull/2074>`_
|
||||||
Performance adjustments in ``handle_request_``
|
Performance adjustments in ``handle_request_``
|
||||||
|
|
||||||
Version 20.12.3 🔷
|
Version 20.12.3
|
||||||
------------------
|
---------------
|
||||||
|
|
||||||
`Current LTS version`
|
`Current LTS version`
|
||||||
|
|
||||||
@@ -350,8 +350,8 @@ Version 19.12.5
|
|||||||
`#2027 <https://github.com/sanic-org/sanic/pull/2027>`_
|
`#2027 <https://github.com/sanic-org/sanic/pull/2027>`_
|
||||||
Remove old chardet requirement, add in hard multidict requirement
|
Remove old chardet requirement, add in hard multidict requirement
|
||||||
|
|
||||||
Version 20.12.0 🔹
|
Version 20.12.0
|
||||||
-----------------
|
---------------
|
||||||
|
|
||||||
**Features**
|
**Features**
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
|
|||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported by contacting the project team at sanic-maintainers@googlegroups.com. All
|
reported by contacting the project team at adam@sanicframework.org. All
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
@@ -71,9 +71,9 @@ To execute only unittests, run ``tox`` with environment like so:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
tox -e py36 -v -- tests/test_config.py
|
|
||||||
# or
|
|
||||||
tox -e py37 -v -- tests/test_config.py
|
tox -e py37 -v -- tests/test_config.py
|
||||||
|
# or
|
||||||
|
tox -e py310 -v -- tests/test_config.py
|
||||||
|
|
||||||
Run lint checks
|
Run lint checks
|
||||||
---------------
|
---------------
|
||||||
|
|||||||
@@ -102,9 +102,6 @@ Installation
|
|||||||
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
|
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
|
||||||
use ``sanic`` with ``ujson`` dependency.
|
use ``sanic`` with ``ujson`` dependency.
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Windows support is currently "experimental" and on a best-effort basis. Multiple workers are also not currently supported on Windows (see `Issue #1517 <https://github.com/sanic-org/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully.
|
|
||||||
|
|
||||||
Hello World Example
|
Hello World Example
|
||||||
-------------------
|
-------------------
|
||||||
|
|||||||
53
SECURITY.md
53
SECURITY.md
@@ -4,31 +4,42 @@
|
|||||||
|
|
||||||
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
|
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
|
||||||
|
|
||||||
| Version | LTS | Supported |
|
|
||||||
| ------- | ------------- | ------------------ |
|
|
||||||
| 20.12 | until 2022-12 | :heavy_check_mark: |
|
|
||||||
| 20.9 | | :x: |
|
|
||||||
| 20.6 | | :x: |
|
|
||||||
| 20.3 | | :x: |
|
|
||||||
| 19.12 | until 2021-12 | :white_check_mark: |
|
|
||||||
| 19.9 | | :x: |
|
|
||||||
| 19.6 | | :x: |
|
|
||||||
| 19.3 | | :x: |
|
|
||||||
| 18.12 | | :x: |
|
|
||||||
| 0.8.3 | | :x: |
|
|
||||||
| 0.7.0 | | :x: |
|
|
||||||
| 0.6.0 | | :x: |
|
|
||||||
| 0.5.4 | | :x: |
|
|
||||||
| 0.4.1 | | :x: |
|
|
||||||
| 0.3.1 | | :x: |
|
|
||||||
| 0.2.0 | | :x: |
|
|
||||||
| 0.1.9 | | :x: |
|
|
||||||
|
|
||||||
:white_check_mark: = security/bug fixes
|
| Version | LTS | Supported |
|
||||||
:heavy_check_mark: = full support
|
| ------- | ------------- | ----------------------- |
|
||||||
|
| 22.12 | until 2024-12 | :white_check_mark: |
|
||||||
|
| 22.9 | | :x: |
|
||||||
|
| 22.6 | | :x: |
|
||||||
|
| 22.3 | | :x: |
|
||||||
|
| 21.12 | until 2023-12 | :ballot_box_with_check: |
|
||||||
|
| 21.9 | | :x: |
|
||||||
|
| 21.6 | | :x: |
|
||||||
|
| 21.3 | | :x: |
|
||||||
|
| 20.12 | | :x: |
|
||||||
|
| 20.9 | | :x: |
|
||||||
|
| 20.6 | | :x: |
|
||||||
|
| 20.3 | | :x: |
|
||||||
|
| 19.12 | | :x: |
|
||||||
|
| 19.9 | | :x: |
|
||||||
|
| 19.6 | | :x: |
|
||||||
|
| 19.3 | | :x: |
|
||||||
|
| 18.12 | | :x: |
|
||||||
|
| 0.8.3 | | :x: |
|
||||||
|
| 0.7.0 | | :x: |
|
||||||
|
| 0.6.0 | | :x: |
|
||||||
|
| 0.5.4 | | :x: |
|
||||||
|
| 0.4.1 | | :x: |
|
||||||
|
| 0.3.1 | | :x: |
|
||||||
|
| 0.2.0 | | :x: |
|
||||||
|
| 0.1.9 | | :x: |
|
||||||
|
|
||||||
|
:ballot_box_with_check: = security/bug fixes
|
||||||
|
:white_check_mark: = full support
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
|
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
|
||||||
|
|
||||||
|
Alternatively, you can send a private message to Adam Hopkins on Discord. Find him on the [Sanic discord server](https://discord.gg/FARQzAEMAA).
|
||||||
|
|
||||||
This will help to not publicize the issue until the team can address it and resolve it.
|
This will help to not publicize the issue until the team can address it and resolve it.
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ codecov:
|
|||||||
ignore:
|
ignore:
|
||||||
- "sanic/__main__.py"
|
- "sanic/__main__.py"
|
||||||
- "sanic/compat.py"
|
- "sanic/compat.py"
|
||||||
- "sanic/reloader_helpers.py"
|
|
||||||
- "sanic/simple.py"
|
- "sanic/simple.py"
|
||||||
- "sanic/utils.py"
|
- "sanic/utils.py"
|
||||||
- "sanic/cli"
|
- "sanic/cli/"
|
||||||
|
- "sanic/pages/"
|
||||||
- ".github/"
|
- ".github/"
|
||||||
- "changelogs/"
|
- "changelogs/"
|
||||||
- "docker/"
|
- "docker/"
|
||||||
|
|||||||
9
docs/_static/custom.css
vendored
9
docs/_static/custom.css
vendored
@@ -2,3 +2,12 @@
|
|||||||
.wy-nav-top {
|
.wy-nav-top {
|
||||||
background: #444444;
|
background: #444444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#changelog section {
|
||||||
|
padding-left: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelog section h2,
|
||||||
|
#changelog section h3 {
|
||||||
|
margin-left: -3rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
📜 Changelog
|
📜 Changelog
|
||||||
============
|
============
|
||||||
|
|
||||||
|
.. mdinclude:: ./releases/22/22.12.md
|
||||||
|
.. mdinclude:: ./releases/22/22.9.md
|
||||||
.. mdinclude:: ./releases/22/22.6.md
|
.. mdinclude:: ./releases/22/22.6.md
|
||||||
.. mdinclude:: ./releases/22/22.3.md
|
.. mdinclude:: ./releases/22/22.3.md
|
||||||
.. mdinclude:: ./releases/21/21.12.md
|
.. mdinclude:: ./releases/21/21.12.md
|
||||||
|
|||||||
55
docs/sanic/releases/22/22.12.md
Normal file
55
docs/sanic/releases/22/22.12.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
## Version 22.12.0 🔶
|
||||||
|
|
||||||
|
_Current version_
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [#2569](https://github.com/sanic-org/sanic/pull/2569) Add `JSONResponse` class with some convenient methods when updating a response object
|
||||||
|
- [#2598](https://github.com/sanic-org/sanic/pull/2598) Change `uvloop` requirement to `>=0.15.0`
|
||||||
|
- [#2609](https://github.com/sanic-org/sanic/pull/2609) Add compatibility with `websockets` v11.0
|
||||||
|
- [#2610](https://github.com/sanic-org/sanic/pull/2610) Kill server early on worker error
|
||||||
|
- Raise deadlock timeout to 30s
|
||||||
|
- [#2617](https://github.com/sanic-org/sanic/pull/2617) Scale number of running server workers
|
||||||
|
- [#2621](https://github.com/sanic-org/sanic/pull/2621) [#2634](https://github.com/sanic-org/sanic/pull/2634) Send `SIGKILL` on subsequent `ctrl+c` to force worker exit
|
||||||
|
- [#2622](https://github.com/sanic-org/sanic/pull/2622) Add API to restart all workers from the multiplexer
|
||||||
|
- [#2624](https://github.com/sanic-org/sanic/pull/2624) Default to `spawn` for all subprocesses unless specifically set:
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
Sanic.start_method = "fork"
|
||||||
|
```
|
||||||
|
- [#2625](https://github.com/sanic-org/sanic/pull/2625) Filename normalisation of form-data/multipart file uploads
|
||||||
|
- [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector:
|
||||||
|
- Remote access to inspect running Sanic instances
|
||||||
|
- TLS support for encrypted calls to Inspector
|
||||||
|
- Authentication to Inspector with API key
|
||||||
|
- Ability to extend Inspector with custom commands
|
||||||
|
- [#2632](https://github.com/sanic-org/sanic/pull/2632) Control order of restart operations
|
||||||
|
- [#2633](https://github.com/sanic-org/sanic/pull/2633) Move reload interval to class variable
|
||||||
|
- [#2636](https://github.com/sanic-org/sanic/pull/2636) Add `priority` to `register_middleware` method
|
||||||
|
- [#2639](https://github.com/sanic-org/sanic/pull/2639) Add `unquote` to `add_route` method
|
||||||
|
- [#2640](https://github.com/sanic-org/sanic/pull/2640) ASGI websockets to receive `text` or `bytes`
|
||||||
|
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- [#2607](https://github.com/sanic-org/sanic/pull/2607) Force socket shutdown before close to allow rebinding
|
||||||
|
- [#2590](https://github.com/sanic-org/sanic/pull/2590) Use actual `StrEnum` in Python 3.11+
|
||||||
|
- [#2615](https://github.com/sanic-org/sanic/pull/2615) Ensure middleware executes only once per request timeout
|
||||||
|
- [#2627](https://github.com/sanic-org/sanic/pull/2627) Crash ASGI application on lifespan failure
|
||||||
|
- [#2635](https://github.com/sanic-org/sanic/pull/2635) Resolve error with low-level server creation on Windows
|
||||||
|
|
||||||
|
|
||||||
|
### Deprecations and Removals
|
||||||
|
|
||||||
|
- [#2608](https://github.com/sanic-org/sanic/pull/2608) [#2630](https://github.com/sanic-org/sanic/pull/2630) Signal conditions and triggers saved on `signal.extra`
|
||||||
|
- [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector
|
||||||
|
- 🚨 *BREAKING CHANGE*: Moves the Inspector to a Sanic app from a simple TCP socket with a custom protocol
|
||||||
|
- *DEPRECATE*: The `--inspect*` commands have been deprecated in favor of `inspect ...` commands
|
||||||
|
- [#2628](https://github.com/sanic-org/sanic/pull/2628) Replace deprecated `distutils.strtobool`
|
||||||
|
|
||||||
|
|
||||||
|
### Developer infrastructure
|
||||||
|
|
||||||
|
- [#2612](https://github.com/sanic-org/sanic/pull/2612) Add CI testing for Python 3.11
|
||||||
|
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
## Version 22.6.0 🔶
|
## Version 22.6.2
|
||||||
|
|
||||||
_Current version_
|
### Bugfixes
|
||||||
|
|
||||||
|
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
|
||||||
|
|
||||||
|
## Version 22.6.1
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- [#2477](https://github.com/sanic-org/sanic/pull/2477) Sanic static directory fails when folder name ends with ".."
|
||||||
|
|
||||||
|
|
||||||
|
## Version 22.6.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode
|
- [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode
|
||||||
|
|||||||
74
docs/sanic/releases/22/22.9.md
Normal file
74
docs/sanic/releases/22/22.9.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
## Version 22.9.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [#2585](https://github.com/sanic-org/sanic/pull/2585) Improved error message when no applications have been registered
|
||||||
|
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- [#2578](https://github.com/sanic-org/sanic/pull/2578) Add certificate loader for in process certificate creation
|
||||||
|
- [#2591](https://github.com/sanic-org/sanic/pull/2591) Do not use sentinel identity for `spawn` compatibility
|
||||||
|
- [#2592](https://github.com/sanic-org/sanic/pull/2592) Fix properties in nested blueprint groups
|
||||||
|
- [#2595](https://github.com/sanic-org/sanic/pull/2595) Introduce sleep interval on new worker reloader
|
||||||
|
|
||||||
|
|
||||||
|
### Deprecations and Removals
|
||||||
|
|
||||||
|
|
||||||
|
### Developer infrastructure
|
||||||
|
|
||||||
|
- [#2588](https://github.com/sanic-org/sanic/pull/2588) Markdown templates on issue forms
|
||||||
|
|
||||||
|
|
||||||
|
### Improved Documentation
|
||||||
|
|
||||||
|
- [#2556](https://github.com/sanic-org/sanic/pull/2556) v22.9 documentation
|
||||||
|
- [#2582](https://github.com/sanic-org/sanic/pull/2582) Cleanup documentation on Windows support
|
||||||
|
|
||||||
|
|
||||||
|
## Version 22.9.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom loads function
|
||||||
|
- [#2490](https://github.com/sanic-org/sanic/pull/2490) Make `WebsocketImplProtocol` async iterable
|
||||||
|
- [#2499](https://github.com/sanic-org/sanic/pull/2499) Sanic Server WorkerManager refactor
|
||||||
|
- [#2506](https://github.com/sanic-org/sanic/pull/2506) Use `pathlib` for path resolution (for static file serving)
|
||||||
|
- [#2508](https://github.com/sanic-org/sanic/pull/2508) Use `path.parts` instead of `match` (for static file serving)
|
||||||
|
- [#2513](https://github.com/sanic-org/sanic/pull/2513) Better request cancel handling
|
||||||
|
- [#2516](https://github.com/sanic-org/sanic/pull/2516) Add request properties for HTTP method info:
|
||||||
|
- `request.is_safe`
|
||||||
|
- `request.is_idempotent`
|
||||||
|
- `request.is_cacheable`
|
||||||
|
- *See* [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) *for more information about when these apply*
|
||||||
|
- [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI
|
||||||
|
- [#2526](https://github.com/sanic-org/sanic/pull/2526) Cache control support for static files for returning 304 when appropriate
|
||||||
|
- [#2533](https://github.com/sanic-org/sanic/pull/2533) Refactor `_static_request_handler`
|
||||||
|
- [#2540](https://github.com/sanic-org/sanic/pull/2540) Add signals before and after handler execution
|
||||||
|
- `http.handler.before`
|
||||||
|
- `http.handler.after`
|
||||||
|
- [#2542](https://github.com/sanic-org/sanic/pull/2542) Add *[redacted]* to CLI :)
|
||||||
|
- [#2546](https://github.com/sanic-org/sanic/pull/2546) Add deprecation warning filter
|
||||||
|
- [#2550](https://github.com/sanic-org/sanic/pull/2550) Middleware priority and performance enhancements
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- [#2495](https://github.com/sanic-org/sanic/pull/2495) Prevent directory traversion with static files
|
||||||
|
- [#2515](https://github.com/sanic-org/sanic/pull/2515) Do not apply double slash to paths in certain static dirs in Blueprints
|
||||||
|
|
||||||
|
### Deprecations and Removals
|
||||||
|
|
||||||
|
- [#2525](https://github.com/sanic-org/sanic/pull/2525) Warn on duplicate route names, will be prevented outright in v23.3
|
||||||
|
- [#2537](https://github.com/sanic-org/sanic/pull/2537) Raise warning and deprecation notice on duplicate exceptions, will be prevented outright in v23.3
|
||||||
|
|
||||||
|
### Developer infrastructure
|
||||||
|
|
||||||
|
- [#2504](https://github.com/sanic-org/sanic/pull/2504) Cleanup test suite
|
||||||
|
- [#2505](https://github.com/sanic-org/sanic/pull/2505) Replace Unsupported Python Version Number from the Contributing Doc
|
||||||
|
- [#2530](https://github.com/sanic-org/sanic/pull/2530) Do not include tests folder in installed package resolver
|
||||||
|
|
||||||
|
### Improved Documentation
|
||||||
|
|
||||||
|
- [#2502](https://github.com/sanic-org/sanic/pull/2502) Fix a few typos
|
||||||
|
- [#2517](https://github.com/sanic-org/sanic/pull/2517) [#2536](https://github.com/sanic-org/sanic/pull/2536) Add some type hints
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import os
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
@@ -13,13 +10,4 @@ async def test(request):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
server_address = "./uds_socket"
|
app.run(unix="./uds_socket")
|
||||||
# Make sure the socket does not already exist
|
|
||||||
try:
|
|
||||||
os.unlink(server_address)
|
|
||||||
except OSError:
|
|
||||||
if os.path.exists(server_address):
|
|
||||||
raise
|
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
||||||
sock.bind(server_address)
|
|
||||||
app.run(sock=sock)
|
|
||||||
|
|||||||
@@ -22,5 +22,7 @@ module = [
|
|||||||
"httptools.*",
|
"httptools.*",
|
||||||
"trustme.*",
|
"trustme.*",
|
||||||
"sanic_routing.*",
|
"sanic_routing.*",
|
||||||
|
"aioquic.*",
|
||||||
|
"html5tagger.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ from sanic.app import Sanic
|
|||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.constants import HTTPMethod
|
from sanic.constants import HTTPMethod
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import HTTPResponse, html, json, text
|
from sanic.response import (
|
||||||
|
HTTPResponse,
|
||||||
|
empty,
|
||||||
|
file,
|
||||||
|
html,
|
||||||
|
json,
|
||||||
|
redirect,
|
||||||
|
text,
|
||||||
|
)
|
||||||
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +23,10 @@ __all__ = (
|
|||||||
"HTTPResponse",
|
"HTTPResponse",
|
||||||
"Request",
|
"Request",
|
||||||
"Websocket",
|
"Websocket",
|
||||||
|
"empty",
|
||||||
|
"file",
|
||||||
"html",
|
"html",
|
||||||
"json",
|
"json",
|
||||||
|
"redirect",
|
||||||
"text",
|
"text",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ if OS_IS_WINDOWS:
|
|||||||
enable_windows_color_support()
|
enable_windows_color_support()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(args=None):
|
||||||
cli = SanicCLI()
|
cli = SanicCLI()
|
||||||
cli.attach()
|
cli.attach()
|
||||||
cli.run()
|
cli.run(args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "22.6.1"
|
__version__ = "23.3.0"
|
||||||
|
|||||||
490
sanic/app.py
490
sanic/app.py
@@ -19,6 +19,7 @@ from collections import defaultdict, deque
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
|
from os import environ
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
@@ -41,13 +42,12 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
from warnings import filterwarnings
|
|
||||||
|
|
||||||
from sanic_routing.exceptions import FinalizationError, NotFound
|
from sanic_routing.exceptions import FinalizationError, NotFound
|
||||||
from sanic_routing.route import Route
|
from sanic_routing.route import Route
|
||||||
|
|
||||||
from sanic.application.ext import setup_ext
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.application.state import ApplicationState, Mode, ServerStage
|
from sanic.application.state import ApplicationState, ServerStage
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.base.root import BaseSanic
|
from sanic.base.root import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
@@ -61,7 +61,7 @@ from sanic.exceptions import (
|
|||||||
URLBuildError,
|
URLBuildError,
|
||||||
)
|
)
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.helpers import _default
|
from sanic.helpers import Default, _default
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
from sanic.log import (
|
from sanic.log import (
|
||||||
LOGGING_CONFIG_DEFAULTS,
|
LOGGING_CONFIG_DEFAULTS,
|
||||||
@@ -69,8 +69,10 @@ from sanic.log import (
|
|||||||
error_logger,
|
error_logger,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
from sanic.middleware import Middleware, MiddlewareLocation
|
||||||
from sanic.mixins.listeners import ListenerEvent
|
from sanic.mixins.listeners import ListenerEvent
|
||||||
from sanic.mixins.runner import RunnerMixin
|
from sanic.mixins.startup import StartupMixin
|
||||||
|
from sanic.mixins.static import StaticHandleMixin
|
||||||
from sanic.models.futures import (
|
from sanic.models.futures import (
|
||||||
FutureException,
|
FutureException,
|
||||||
FutureListener,
|
FutureListener,
|
||||||
@@ -78,7 +80,6 @@ from sanic.models.futures import (
|
|||||||
FutureRegistry,
|
FutureRegistry,
|
||||||
FutureRoute,
|
FutureRoute,
|
||||||
FutureSignal,
|
FutureSignal,
|
||||||
FutureStatic,
|
|
||||||
)
|
)
|
||||||
from sanic.models.handler_types import ListenerType, MiddlewareType
|
from sanic.models.handler_types import ListenerType, MiddlewareType
|
||||||
from sanic.models.handler_types import Sanic as SanicVar
|
from sanic.models.handler_types import Sanic as SanicVar
|
||||||
@@ -88,6 +89,9 @@ from sanic.router import Router
|
|||||||
from sanic.server.websockets.impl import ConnectionClosed
|
from sanic.server.websockets.impl import ConnectionClosed
|
||||||
from sanic.signals import Signal, SignalRouter
|
from sanic.signals import Signal, SignalRouter
|
||||||
from sanic.touchup import TouchUp, TouchUpMeta
|
from sanic.touchup import TouchUp, TouchUpMeta
|
||||||
|
from sanic.types.shared_ctx import SharedContext
|
||||||
|
from sanic.worker.inspector import Inspector
|
||||||
|
from sanic.worker.manager import WorkerManager
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -101,10 +105,8 @@ if TYPE_CHECKING:
|
|||||||
if OS_IS_WINDOWS: # no cov
|
if OS_IS_WINDOWS: # no cov
|
||||||
enable_windows_color_support()
|
enable_windows_color_support()
|
||||||
|
|
||||||
filterwarnings("once", category=DeprecationWarning)
|
|
||||||
|
|
||||||
|
class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||||
class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|
||||||
"""
|
"""
|
||||||
The main application instance
|
The main application instance
|
||||||
"""
|
"""
|
||||||
@@ -128,6 +130,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
"_future_routes",
|
"_future_routes",
|
||||||
"_future_signals",
|
"_future_signals",
|
||||||
"_future_statics",
|
"_future_statics",
|
||||||
|
"_inspector",
|
||||||
|
"_manager",
|
||||||
"_state",
|
"_state",
|
||||||
"_task_registry",
|
"_task_registry",
|
||||||
"_test_client",
|
"_test_client",
|
||||||
@@ -137,28 +141,31 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
"configure_logging",
|
"configure_logging",
|
||||||
"ctx",
|
"ctx",
|
||||||
"error_handler",
|
"error_handler",
|
||||||
|
"inspector_class",
|
||||||
"go_fast",
|
"go_fast",
|
||||||
"listeners",
|
"listeners",
|
||||||
|
"multiplexer",
|
||||||
"named_request_middleware",
|
"named_request_middleware",
|
||||||
"named_response_middleware",
|
"named_response_middleware",
|
||||||
"request_class",
|
"request_class",
|
||||||
"request_middleware",
|
"request_middleware",
|
||||||
"response_middleware",
|
"response_middleware",
|
||||||
"router",
|
"router",
|
||||||
|
"shared_ctx",
|
||||||
"signal_router",
|
"signal_router",
|
||||||
"sock",
|
"sock",
|
||||||
"strict_slashes",
|
"strict_slashes",
|
||||||
"websocket_enabled",
|
"websocket_enabled",
|
||||||
"websocket_tasks",
|
"websocket_tasks",
|
||||||
|
"wrappers",
|
||||||
)
|
)
|
||||||
|
|
||||||
_app_registry: Dict[str, "Sanic"] = {}
|
_app_registry: Dict[str, "Sanic"] = {}
|
||||||
_uvloop_setting = None # TODO: Remove in v22.6
|
|
||||||
test_mode = False
|
test_mode = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str = None,
|
name: Optional[str] = None,
|
||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
ctx: Optional[Any] = None,
|
ctx: Optional[Any] = None,
|
||||||
router: Optional[Router] = None,
|
router: Optional[Router] = None,
|
||||||
@@ -171,9 +178,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
dumps: Optional[Callable[..., AnyStr]] = None,
|
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||||
loads: Optional[Callable[..., Any]] = None,
|
loads: Optional[Callable[..., Any]] = None,
|
||||||
|
inspector: bool = False,
|
||||||
|
inspector_class: Optional[Type[Inspector]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
if configure_logging:
|
if configure_logging:
|
||||||
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
|
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
|
||||||
@@ -187,12 +195,16 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
# First setup config
|
# First setup config
|
||||||
self.config: Config = config or Config(env_prefix=env_prefix)
|
self.config: Config = config or Config(env_prefix=env_prefix)
|
||||||
|
if inspector:
|
||||||
|
self.config.INSPECTOR = inspector
|
||||||
|
|
||||||
# Then we can do the rest
|
# Then we can do the rest
|
||||||
self._asgi_client: Any = None
|
self._asgi_client: Any = None
|
||||||
self._blueprint_order: List[Blueprint] = []
|
self._blueprint_order: List[Blueprint] = []
|
||||||
self._delayed_tasks: List[str] = []
|
self._delayed_tasks: List[str] = []
|
||||||
self._future_registry: FutureRegistry = FutureRegistry()
|
self._future_registry: FutureRegistry = FutureRegistry()
|
||||||
|
self._inspector: Optional[Inspector] = None
|
||||||
|
self._manager: Optional[WorkerManager] = None
|
||||||
self._state: ApplicationState = ApplicationState(app=self)
|
self._state: ApplicationState = ApplicationState(app=self)
|
||||||
self._task_registry: Dict[str, Task] = {}
|
self._task_registry: Dict[str, Task] = {}
|
||||||
self._test_client: Any = None
|
self._test_client: Any = None
|
||||||
@@ -203,6 +215,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
self.configure_logging: bool = configure_logging
|
self.configure_logging: bool = configure_logging
|
||||||
self.ctx: Any = 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.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
||||||
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
@@ -210,6 +223,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
self.request_middleware: Deque[MiddlewareType] = deque()
|
self.request_middleware: Deque[MiddlewareType] = deque()
|
||||||
self.response_middleware: Deque[MiddlewareType] = deque()
|
self.response_middleware: Deque[MiddlewareType] = deque()
|
||||||
self.router: Router = router or Router()
|
self.router: Router = router or Router()
|
||||||
|
self.shared_ctx: SharedContext = SharedContext()
|
||||||
self.signal_router: SignalRouter = signal_router or SignalRouter()
|
self.signal_router: SignalRouter = signal_router or SignalRouter()
|
||||||
self.sock: Optional[socket] = None
|
self.sock: Optional[socket] = None
|
||||||
self.strict_slashes: bool = strict_slashes
|
self.strict_slashes: bool = strict_slashes
|
||||||
@@ -243,7 +257,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return get_running_loop()
|
return get_running_loop()
|
||||||
except RuntimeError:
|
except RuntimeError: # no cov
|
||||||
if sys.version_info > (3, 10):
|
if sys.version_info > (3, 10):
|
||||||
return asyncio.get_event_loop_policy().get_event_loop()
|
return asyncio.get_event_loop_policy().get_event_loop()
|
||||||
else:
|
else:
|
||||||
@@ -282,8 +296,12 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
return listener
|
return listener
|
||||||
|
|
||||||
def register_middleware(
|
def register_middleware(
|
||||||
self, middleware: MiddlewareType, attach_to: str = "request"
|
self,
|
||||||
) -> MiddlewareType:
|
middleware: Union[MiddlewareType, Middleware],
|
||||||
|
attach_to: str = "request",
|
||||||
|
*,
|
||||||
|
priority: Union[Default, int] = _default,
|
||||||
|
) -> Union[MiddlewareType, Middleware]:
|
||||||
"""
|
"""
|
||||||
Register an application level middleware that will be attached
|
Register an application level middleware that will be attached
|
||||||
to all the API URLs registered under this application.
|
to all the API URLs registered under this application.
|
||||||
@@ -299,19 +317,37 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
**response** - Invoke before the response is returned back
|
**response** - Invoke before the response is returned back
|
||||||
:return: decorated method
|
:return: decorated method
|
||||||
"""
|
"""
|
||||||
if attach_to == "request":
|
retval = middleware
|
||||||
|
location = MiddlewareLocation[attach_to.upper()]
|
||||||
|
|
||||||
|
if not isinstance(middleware, Middleware):
|
||||||
|
middleware = Middleware(
|
||||||
|
middleware,
|
||||||
|
location=location,
|
||||||
|
priority=priority if isinstance(priority, int) else 0,
|
||||||
|
)
|
||||||
|
elif middleware.priority != priority and isinstance(priority, int):
|
||||||
|
middleware = Middleware(
|
||||||
|
middleware.func,
|
||||||
|
location=middleware.location,
|
||||||
|
priority=priority,
|
||||||
|
)
|
||||||
|
|
||||||
|
if location is MiddlewareLocation.REQUEST:
|
||||||
if middleware not in self.request_middleware:
|
if middleware not in self.request_middleware:
|
||||||
self.request_middleware.append(middleware)
|
self.request_middleware.append(middleware)
|
||||||
if attach_to == "response":
|
if location is MiddlewareLocation.RESPONSE:
|
||||||
if middleware not in self.response_middleware:
|
if middleware not in self.response_middleware:
|
||||||
self.response_middleware.appendleft(middleware)
|
self.response_middleware.appendleft(middleware)
|
||||||
return middleware
|
return retval
|
||||||
|
|
||||||
def register_named_middleware(
|
def register_named_middleware(
|
||||||
self,
|
self,
|
||||||
middleware: MiddlewareType,
|
middleware: MiddlewareType,
|
||||||
route_names: Iterable[str],
|
route_names: Iterable[str],
|
||||||
attach_to: str = "request",
|
attach_to: str = "request",
|
||||||
|
*,
|
||||||
|
priority: Union[Default, int] = _default,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Method for attaching middleware to specific routes. This is mainly an
|
Method for attaching middleware to specific routes. This is mainly an
|
||||||
@@ -325,19 +361,35 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
defaults to "request"
|
defaults to "request"
|
||||||
:type attach_to: str, optional
|
:type attach_to: str, optional
|
||||||
"""
|
"""
|
||||||
if attach_to == "request":
|
retval = middleware
|
||||||
|
location = MiddlewareLocation[attach_to.upper()]
|
||||||
|
|
||||||
|
if not isinstance(middleware, Middleware):
|
||||||
|
middleware = Middleware(
|
||||||
|
middleware,
|
||||||
|
location=location,
|
||||||
|
priority=priority if isinstance(priority, int) else 0,
|
||||||
|
)
|
||||||
|
elif middleware.priority != priority and isinstance(priority, int):
|
||||||
|
middleware = Middleware(
|
||||||
|
middleware.func,
|
||||||
|
location=middleware.location,
|
||||||
|
priority=priority,
|
||||||
|
)
|
||||||
|
|
||||||
|
if location is MiddlewareLocation.REQUEST:
|
||||||
for _rn in route_names:
|
for _rn in route_names:
|
||||||
if _rn not in self.named_request_middleware:
|
if _rn not in self.named_request_middleware:
|
||||||
self.named_request_middleware[_rn] = deque()
|
self.named_request_middleware[_rn] = deque()
|
||||||
if middleware not in self.named_request_middleware[_rn]:
|
if middleware not in self.named_request_middleware[_rn]:
|
||||||
self.named_request_middleware[_rn].append(middleware)
|
self.named_request_middleware[_rn].append(middleware)
|
||||||
if attach_to == "response":
|
if location is MiddlewareLocation.RESPONSE:
|
||||||
for _rn in route_names:
|
for _rn in route_names:
|
||||||
if _rn not in self.named_response_middleware:
|
if _rn not in self.named_response_middleware:
|
||||||
self.named_response_middleware[_rn] = deque()
|
self.named_response_middleware[_rn] = deque()
|
||||||
if middleware not in self.named_response_middleware[_rn]:
|
if middleware not in self.named_response_middleware[_rn]:
|
||||||
self.named_response_middleware[_rn].appendleft(middleware)
|
self.named_response_middleware[_rn].appendleft(middleware)
|
||||||
return middleware
|
return retval
|
||||||
|
|
||||||
def _apply_exception_handler(
|
def _apply_exception_handler(
|
||||||
self,
|
self,
|
||||||
@@ -384,15 +436,12 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
routes = [routes]
|
routes = [routes]
|
||||||
|
|
||||||
for r in routes:
|
for r in routes:
|
||||||
r.ctx.websocket = websocket
|
r.extra.websocket = websocket
|
||||||
r.ctx.static = params.get("static", False)
|
r.extra.static = params.get("static", False)
|
||||||
r.ctx.__dict__.update(ctx)
|
r.ctx.__dict__.update(ctx)
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
def _apply_static(self, static: FutureStatic) -> Route:
|
|
||||||
return self._register_static(static)
|
|
||||||
|
|
||||||
def _apply_middleware(
|
def _apply_middleware(
|
||||||
self,
|
self,
|
||||||
middleware: FutureMiddleware,
|
middleware: FutureMiddleware,
|
||||||
@@ -458,9 +507,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
def blueprint(
|
def blueprint(
|
||||||
self,
|
self,
|
||||||
blueprint: Union[
|
blueprint: Union[Blueprint, Iterable[Blueprint], BlueprintGroup],
|
||||||
Blueprint, List[Blueprint], Tuple[Blueprint], BlueprintGroup
|
|
||||||
],
|
|
||||||
**options: Any,
|
**options: Any,
|
||||||
):
|
):
|
||||||
"""Register a blueprint on the application.
|
"""Register a blueprint on the application.
|
||||||
@@ -469,21 +516,20 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
:param options: option dictionary with blueprint defaults
|
:param options: option dictionary with blueprint defaults
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
if isinstance(blueprint, (Iterable, BlueprintGroup)):
|
||||||
for item in blueprint:
|
for item in blueprint:
|
||||||
params = {**options}
|
params = {**options}
|
||||||
if isinstance(blueprint, BlueprintGroup):
|
if isinstance(blueprint, BlueprintGroup):
|
||||||
if blueprint.url_prefix:
|
merge_from = [
|
||||||
merge_from = [
|
options.get("url_prefix", ""),
|
||||||
options.get("url_prefix", ""),
|
blueprint.url_prefix or "",
|
||||||
blueprint.url_prefix,
|
]
|
||||||
]
|
if not isinstance(item, BlueprintGroup):
|
||||||
if not isinstance(item, BlueprintGroup):
|
merge_from.append(item.url_prefix or "")
|
||||||
merge_from.append(item.url_prefix or "")
|
merged_prefix = "/".join(
|
||||||
merged_prefix = "/".join(
|
u.strip("/") for u in merge_from if u
|
||||||
u.strip("/") for u in merge_from
|
).rstrip("/")
|
||||||
).rstrip("/")
|
params["url_prefix"] = f"/{merged_prefix}"
|
||||||
params["url_prefix"] = f"/{merged_prefix}"
|
|
||||||
|
|
||||||
for _attr in ["version", "strict_slashes"]:
|
for _attr in ["version", "strict_slashes"]:
|
||||||
if getattr(item, _attr) is None:
|
if getattr(item, _attr) is None:
|
||||||
@@ -581,7 +627,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
uri = route.path
|
uri = route.path
|
||||||
|
|
||||||
if getattr(route.ctx, "static", None):
|
if getattr(route.extra, "static", None):
|
||||||
filename = kwargs.pop("filename", "")
|
filename = kwargs.pop("filename", "")
|
||||||
# it's static folder
|
# it's static folder
|
||||||
if "__file_uri__" in uri:
|
if "__file_uri__" in uri:
|
||||||
@@ -614,18 +660,18 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
host = kwargs.pop("_host", None)
|
host = kwargs.pop("_host", None)
|
||||||
external = kwargs.pop("_external", False) or bool(host)
|
external = kwargs.pop("_external", False) or bool(host)
|
||||||
scheme = kwargs.pop("_scheme", "")
|
scheme = kwargs.pop("_scheme", "")
|
||||||
if route.ctx.hosts and external:
|
if route.extra.hosts and external:
|
||||||
if not host and len(route.ctx.hosts) > 1:
|
if not host and len(route.extra.hosts) > 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Host is ambiguous: {', '.join(route.ctx.hosts)}"
|
f"Host is ambiguous: {', '.join(route.extra.hosts)}"
|
||||||
)
|
)
|
||||||
elif host and host not in route.ctx.hosts:
|
elif host and host not in route.extra.hosts:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Requested host ({host}) is not available for this "
|
f"Requested host ({host}) is not available for this "
|
||||||
f"route: {route.ctx.hosts}"
|
f"route: {route.extra.hosts}"
|
||||||
)
|
)
|
||||||
elif not host:
|
elif not host:
|
||||||
host = list(route.ctx.hosts)[0]
|
host = list(route.extra.hosts)[0]
|
||||||
|
|
||||||
if scheme and not external:
|
if scheme and not external:
|
||||||
raise ValueError("When specifying _scheme, _external must be True")
|
raise ValueError("When specifying _scheme, _external must be True")
|
||||||
@@ -701,7 +747,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
async def handle_exception(
|
async def handle_exception(
|
||||||
self, request: Request, exception: BaseException
|
self,
|
||||||
|
request: Request,
|
||||||
|
exception: BaseException,
|
||||||
|
run_middleware: bool = True,
|
||||||
): # no cov
|
): # no cov
|
||||||
"""
|
"""
|
||||||
A handler that catches specific exceptions and outputs a response.
|
A handler that catches specific exceptions and outputs a response.
|
||||||
@@ -710,6 +759,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
:param exception: The exception that was raised
|
:param exception: The exception that was raised
|
||||||
:raises ServerError: response 500
|
:raises ServerError: response 500
|
||||||
"""
|
"""
|
||||||
|
response = None
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.exception",
|
"http.lifecycle.exception",
|
||||||
inline=True,
|
inline=True,
|
||||||
@@ -750,9 +800,11 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
response = await self._run_request_middleware(
|
if run_middleware:
|
||||||
request, request_name=None
|
middleware = (
|
||||||
)
|
request.route and request.route.extra.request_middleware
|
||||||
|
) or self.request_middleware
|
||||||
|
response = await self._run_request_middleware(request, middleware)
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
try:
|
try:
|
||||||
@@ -824,6 +876,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
:param request: HTTP Request object
|
:param request: HTTP Request object
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.handle",
|
"http.lifecycle.handle",
|
||||||
inline=True,
|
inline=True,
|
||||||
@@ -832,9 +886,15 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
# Define `response` var here to remove warnings about
|
# Define `response` var here to remove warnings about
|
||||||
# allocation before assignment below.
|
# allocation before assignment below.
|
||||||
response = None
|
response: Optional[
|
||||||
|
Union[
|
||||||
|
BaseHTTPResponse,
|
||||||
|
Coroutine[Any, Any, Optional[BaseHTTPResponse]],
|
||||||
|
ResponseStream,
|
||||||
|
]
|
||||||
|
] = None
|
||||||
|
run_middleware = True
|
||||||
try:
|
try:
|
||||||
|
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.routing.before",
|
"http.routing.before",
|
||||||
inline=True,
|
inline=True,
|
||||||
@@ -864,9 +924,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
if (
|
if (
|
||||||
request.stream
|
request.stream
|
||||||
and request.stream.request_body
|
and request.stream.request_body
|
||||||
and not route.ctx.ignore_body
|
and not route.extra.ignore_body
|
||||||
):
|
):
|
||||||
|
|
||||||
if hasattr(handler, "is_stream"):
|
if hasattr(handler, "is_stream"):
|
||||||
# Streaming handler: lift the size limit
|
# Streaming handler: lift the size limit
|
||||||
request.stream.request_max_size = float("inf")
|
request.stream.request_max_size = float("inf")
|
||||||
@@ -877,9 +936,11 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
response = await self._run_request_middleware(
|
run_middleware = False
|
||||||
request, request_name=route.name
|
if request.route.extra.request_middleware:
|
||||||
)
|
response = await self._run_request_middleware(
|
||||||
|
request, request.route.extra.request_middleware
|
||||||
|
)
|
||||||
|
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
@@ -896,9 +957,19 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Run response handler
|
# Run response handler
|
||||||
|
await self.dispatch(
|
||||||
|
"http.handler.before",
|
||||||
|
inline=True,
|
||||||
|
context={"request": request},
|
||||||
|
)
|
||||||
response = handler(request, **request.match_info)
|
response = handler(request, **request.match_info)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
await self.dispatch(
|
||||||
|
"http.handler.after",
|
||||||
|
inline=True,
|
||||||
|
context={"request": request},
|
||||||
|
)
|
||||||
|
|
||||||
if request.responded:
|
if request.responded:
|
||||||
if response is not None:
|
if response is not None:
|
||||||
@@ -910,7 +981,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
if request.stream is not None:
|
if request.stream is not None:
|
||||||
response = request.stream.response
|
response = request.stream.response
|
||||||
elif response is not None:
|
elif response is not None:
|
||||||
response = await request.respond(response)
|
response = await request.respond(response) # type: ignore
|
||||||
elif not hasattr(handler, "is_websocket"):
|
elif not hasattr(handler, "is_websocket"):
|
||||||
response = request.stream.response # type: ignore
|
response = request.stream.response # type: ignore
|
||||||
|
|
||||||
@@ -949,7 +1020,9 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Response Generation Failed
|
# Response Generation Failed
|
||||||
await self.handle_exception(request, e)
|
await self.handle_exception(
|
||||||
|
request, e, run_middleware=run_middleware
|
||||||
|
)
|
||||||
|
|
||||||
async def _websocket_handler(
|
async def _websocket_handler(
|
||||||
self, handler, request, *args, subprotocols=None, **kwargs
|
self, handler, request, *args, subprotocols=None, **kwargs
|
||||||
@@ -999,7 +1072,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
def asgi_client(self): # noqa
|
def asgi_client(self): # noqa
|
||||||
"""
|
"""
|
||||||
A testing client that uses ASGI to reach into the application to
|
A testing client that uses ASGI to reach into the application to
|
||||||
execute hanlers.
|
execute handlers.
|
||||||
|
|
||||||
:return: testing client
|
:return: testing client
|
||||||
:rtype: :class:`SanicASGITestClient`
|
:rtype: :class:`SanicASGITestClient`
|
||||||
@@ -1018,86 +1091,72 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
async def _run_request_middleware(
|
async def _run_request_middleware(
|
||||||
self, request, request_name=None
|
self, request, middleware_collection
|
||||||
): # no cov
|
): # no cov
|
||||||
# The if improves speed. I don't know why
|
request._request_middleware_started = True
|
||||||
named_middleware = self.named_request_middleware.get(
|
|
||||||
request_name, deque()
|
|
||||||
)
|
|
||||||
applicable_middleware = self.request_middleware + named_middleware
|
|
||||||
|
|
||||||
# request.request_middleware_started is meant as a stop-gap solution
|
for middleware in middleware_collection:
|
||||||
# until RFC 1630 is adopted
|
await self.dispatch(
|
||||||
if applicable_middleware and not request.request_middleware_started:
|
"http.middleware.before",
|
||||||
request.request_middleware_started = True
|
inline=True,
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"response": None,
|
||||||
|
},
|
||||||
|
condition={"attach_to": "request"},
|
||||||
|
)
|
||||||
|
|
||||||
for middleware in applicable_middleware:
|
response = middleware(request)
|
||||||
await self.dispatch(
|
if isawaitable(response):
|
||||||
"http.middleware.before",
|
response = await response
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": None,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
response = middleware(request)
|
await self.dispatch(
|
||||||
if isawaitable(response):
|
"http.middleware.after",
|
||||||
response = await response
|
inline=True,
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"response": None,
|
||||||
|
},
|
||||||
|
condition={"attach_to": "request"},
|
||||||
|
)
|
||||||
|
|
||||||
await self.dispatch(
|
if response:
|
||||||
"http.middleware.after",
|
return response
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": None,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
return response
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _run_response_middleware(
|
async def _run_response_middleware(
|
||||||
self, request, response, request_name=None
|
self, request, response, middleware_collection
|
||||||
): # no cov
|
): # no cov
|
||||||
named_middleware = self.named_response_middleware.get(
|
for middleware in middleware_collection:
|
||||||
request_name, deque()
|
await self.dispatch(
|
||||||
)
|
"http.middleware.before",
|
||||||
applicable_middleware = self.response_middleware + named_middleware
|
inline=True,
|
||||||
if applicable_middleware:
|
context={
|
||||||
for middleware in applicable_middleware:
|
"request": request,
|
||||||
await self.dispatch(
|
"response": response,
|
||||||
"http.middleware.before",
|
},
|
||||||
inline=True,
|
condition={"attach_to": "response"},
|
||||||
context={
|
)
|
||||||
"request": request,
|
|
||||||
"response": response,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "response"},
|
|
||||||
)
|
|
||||||
|
|
||||||
_response = middleware(request, response)
|
_response = middleware(request, response)
|
||||||
if isawaitable(_response):
|
if isawaitable(_response):
|
||||||
_response = await _response
|
_response = await _response
|
||||||
|
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.middleware.after",
|
"http.middleware.after",
|
||||||
inline=True,
|
inline=True,
|
||||||
context={
|
context={
|
||||||
"request": request,
|
"request": request,
|
||||||
"response": _response if _response else response,
|
"response": _response if _response else response,
|
||||||
},
|
},
|
||||||
condition={"attach_to": "response"},
|
condition={"attach_to": "response"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if _response:
|
if _response:
|
||||||
response = _response
|
response = _response
|
||||||
if isinstance(response, BaseHTTPResponse):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
response = request.stream.respond(response)
|
response = request.stream.respond(response)
|
||||||
break
|
break
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _build_endpoint_name(self, *parts):
|
def _build_endpoint_name(self, *parts):
|
||||||
@@ -1184,7 +1243,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
*,
|
*,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
register: bool = True,
|
register: bool = True,
|
||||||
) -> Optional[Task]:
|
) -> Optional[Task[Any]]:
|
||||||
"""
|
"""
|
||||||
Schedule a task to run later, after the loop has started.
|
Schedule a task to run later, after the loop has started.
|
||||||
Different from asyncio.ensure_future in that it does not
|
Different from asyncio.ensure_future in that it does not
|
||||||
@@ -1194,7 +1253,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
`See user guide re: background tasks
|
`See user guide re: background tasks
|
||||||
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__
|
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__
|
||||||
|
|
||||||
:param task: future, couroutine or awaitable
|
:param task: future, coroutine or awaitable
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
loop = self.loop # Will raise SanicError if loop is not started
|
loop = self.loop # Will raise SanicError if loop is not started
|
||||||
@@ -1315,7 +1374,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
self.config.update_config(config)
|
self.config.update_config(config)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def asgi(self):
|
def asgi(self) -> bool:
|
||||||
return self.state.asgi
|
return self.state.asgi
|
||||||
|
|
||||||
@asgi.setter
|
@asgi.setter
|
||||||
@@ -1326,18 +1385,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
def debug(self):
|
def debug(self):
|
||||||
return self.state.is_debug
|
return self.state.is_debug
|
||||||
|
|
||||||
@debug.setter
|
|
||||||
def debug(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Setting the value of a Sanic application's debug value directly "
|
|
||||||
"is deprecated and will be removed in v22.9. Please set it using "
|
|
||||||
"the CLI, app.run, app.prepare, or directly set "
|
|
||||||
"app.state.mode to Mode.DEBUG.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
mode = Mode.DEBUG if value else Mode.PRODUCTION
|
|
||||||
self.state.mode = mode
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_reload(self):
|
def auto_reload(self):
|
||||||
return self.config.AUTO_RELOAD
|
return self.config.AUTO_RELOAD
|
||||||
@@ -1345,6 +1392,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
@auto_reload.setter
|
@auto_reload.setter
|
||||||
def auto_reload(self, value: bool):
|
def auto_reload(self, value: bool):
|
||||||
self.config.AUTO_RELOAD = value
|
self.config.AUTO_RELOAD = value
|
||||||
|
self.state.auto_reload = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> ApplicationState: # type: ignore
|
def state(self) -> ApplicationState: # type: ignore
|
||||||
@@ -1353,58 +1401,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
|
||||||
def is_running(self):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_running property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
return self.state.is_running
|
|
||||||
|
|
||||||
@is_running.setter
|
|
||||||
def is_running(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_running property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
self.state.is_running = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_stopping(self):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_stopping property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
return self.state.is_stopping
|
|
||||||
|
|
||||||
@is_stopping.setter
|
|
||||||
def is_stopping(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_stopping property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
self.state.is_stopping = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reload_dirs(self):
|
def reload_dirs(self):
|
||||||
return self.state.reload_dirs
|
return self.state.reload_dirs
|
||||||
@@ -1462,6 +1458,18 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
cls._app_registry[name] = app
|
cls._app_registry[name] = app
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def unregister_app(cls, app: "Sanic") -> None:
|
||||||
|
"""
|
||||||
|
Unregister a Sanic instance
|
||||||
|
"""
|
||||||
|
if not isinstance(app, cls):
|
||||||
|
raise SanicException("Registered app must be an instance of Sanic")
|
||||||
|
|
||||||
|
name = app.name
|
||||||
|
if name in cls._app_registry:
|
||||||
|
del cls._app_registry[name]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_app(
|
def get_app(
|
||||||
cls, name: Optional[str] = None, *, force_create: bool = False
|
cls, name: Optional[str] = None, *, force_create: bool = False
|
||||||
@@ -1481,9 +1489,28 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
try:
|
try:
|
||||||
return cls._app_registry[name]
|
return cls._app_registry[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
if name == "__main__":
|
||||||
|
return cls.get_app("__mp_main__", force_create=force_create)
|
||||||
if force_create:
|
if force_create:
|
||||||
return cls(name)
|
return cls(name)
|
||||||
raise SanicException(f'Sanic app name "{name}" not found.')
|
raise SanicException(
|
||||||
|
f"Sanic app name '{name}' not found.\n"
|
||||||
|
"App instantiation must occur outside "
|
||||||
|
"if __name__ == '__main__' "
|
||||||
|
"block or by using an AppLoader.\nSee "
|
||||||
|
"https://sanic.dev/en/guide/deployment/app-loader.html"
|
||||||
|
" for more details."
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_uvloop_conflict(cls) -> None:
|
||||||
|
values = {app.config.USE_UVLOOP for app in cls._app_registry.values()}
|
||||||
|
if len(values) > 1:
|
||||||
|
error_logger.warning(
|
||||||
|
"It looks like you're running several apps with different "
|
||||||
|
"uvloop settings. This is not supported and may lead to "
|
||||||
|
"unintended behaviour."
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
@@ -1495,6 +1522,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
except FinalizationError as e:
|
except FinalizationError as e:
|
||||||
if not Sanic.test_mode:
|
if not Sanic.test_mode:
|
||||||
raise e
|
raise e
|
||||||
|
self.finalize_middleware()
|
||||||
|
|
||||||
def signalize(self, allow_fail_builtin=True):
|
def signalize(self, allow_fail_builtin=True):
|
||||||
self.signal_router.allow_fail_builtin = allow_fail_builtin
|
self.signal_router.allow_fail_builtin = allow_fail_builtin
|
||||||
@@ -1514,24 +1542,26 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
if self.state.is_debug and self.config.TOUCHUP is not True:
|
if self.state.is_debug and self.config.TOUCHUP is not True:
|
||||||
self.config.TOUCHUP = False
|
self.config.TOUCHUP = False
|
||||||
elif self.config.TOUCHUP is _default:
|
elif isinstance(self.config.TOUCHUP, Default):
|
||||||
self.config.TOUCHUP = True
|
self.config.TOUCHUP = True
|
||||||
|
|
||||||
# Setup routers
|
# Setup routers
|
||||||
self.signalize(self.config.TOUCHUP)
|
self.signalize(self.config.TOUCHUP)
|
||||||
self.finalize()
|
self.finalize()
|
||||||
|
|
||||||
# TODO: Replace in v22.6 to check against apps in app registry
|
route_names = [route.name for route in self.router.routes]
|
||||||
if (
|
duplicates = {
|
||||||
self.__class__._uvloop_setting is not None
|
name for name in route_names if route_names.count(name) > 1
|
||||||
and self.__class__._uvloop_setting != self.config.USE_UVLOOP
|
}
|
||||||
):
|
if duplicates:
|
||||||
error_logger.warning(
|
names = ", ".join(duplicates)
|
||||||
"It looks like you're running several apps with different "
|
deprecation(
|
||||||
"uvloop settings. This is not supported and may lead to "
|
f"Duplicate route names detected: {names}. In the future, "
|
||||||
"unintended behaviour."
|
"Sanic will enforce uniqueness in route naming.",
|
||||||
|
23.3,
|
||||||
)
|
)
|
||||||
self.__class__._uvloop_setting = self.config.USE_UVLOOP
|
|
||||||
|
Sanic._check_uvloop_conflict()
|
||||||
|
|
||||||
# Startup time optimizations
|
# Startup time optimizations
|
||||||
if self.state.primary:
|
if self.state.primary:
|
||||||
@@ -1542,6 +1572,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
self.state.is_started = True
|
self.state.is_started = True
|
||||||
|
|
||||||
|
def ack(self):
|
||||||
|
if hasattr(self, "multiplexer"):
|
||||||
|
self.multiplexer.ack()
|
||||||
|
|
||||||
async def _server_event(
|
async def _server_event(
|
||||||
self,
|
self,
|
||||||
concern: str,
|
concern: str,
|
||||||
@@ -1570,3 +1604,43 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
"loop": loop,
|
"loop": loop,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
# Process Management
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def refresh(
|
||||||
|
self,
|
||||||
|
passthru: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
registered = self.__class__.get_app(self.name)
|
||||||
|
if self is not registered:
|
||||||
|
if not registered.state.server_info:
|
||||||
|
registered.state.server_info = self.state.server_info
|
||||||
|
self = registered
|
||||||
|
if passthru:
|
||||||
|
for attr, info in passthru.items():
|
||||||
|
if isinstance(info, dict):
|
||||||
|
for key, value in info.items():
|
||||||
|
setattr(getattr(self, attr), key, value)
|
||||||
|
else:
|
||||||
|
setattr(self, attr, info)
|
||||||
|
if hasattr(self, "multiplexer"):
|
||||||
|
self.shared_ctx.lock()
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inspector(self):
|
||||||
|
if environ.get("SANIC_WORKER_PROCESS") or not self._inspector:
|
||||||
|
raise SanicException(
|
||||||
|
"Can only access the inspector from the main process"
|
||||||
|
)
|
||||||
|
return self._inspector
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manager(self):
|
||||||
|
if environ.get("SANIC_WORKER_PROCESS") or not self._manager:
|
||||||
|
raise SanicException(
|
||||||
|
"Can only access the manager from the main process"
|
||||||
|
)
|
||||||
|
return self._manager
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
from enum import Enum, IntEnum, auto
|
from enum import Enum, IntEnum, auto
|
||||||
|
|
||||||
|
|
||||||
class StrEnum(str, Enum):
|
class StrEnum(str, Enum): # no cov
|
||||||
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
|
def __eq__(self, value: object) -> bool:
|
||||||
|
value = str(value).upper()
|
||||||
|
return super().__eq__(value)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.value)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class Server(StrEnum):
|
class Server(StrEnum):
|
||||||
SANIC = auto()
|
SANIC = auto()
|
||||||
ASGI = auto()
|
ASGI = auto()
|
||||||
GUNICORN = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class Mode(StrEnum):
|
class Mode(StrEnum):
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ from typing import TYPE_CHECKING
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
try:
|
|
||||||
from sanic_ext import Extend # type: ignore
|
|
||||||
except ImportError:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
||||||
if not app.config.AUTO_EXTEND:
|
if not app.config.AUTO_EXTEND:
|
||||||
@@ -22,7 +17,7 @@ def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
|||||||
with suppress(ModuleNotFoundError):
|
with suppress(ModuleNotFoundError):
|
||||||
sanic_ext = import_module("sanic_ext")
|
sanic_ext = import_module("sanic_ext")
|
||||||
|
|
||||||
if not sanic_ext:
|
if not sanic_ext: # no cov
|
||||||
if fail:
|
if fail:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Sanic Extensions is not installed. You can add it to your "
|
"Sanic Extensions is not installed. You can add it to your "
|
||||||
@@ -33,7 +28,7 @@ def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not getattr(app, "_ext", None):
|
if not getattr(app, "_ext", None):
|
||||||
Ext: Extend = getattr(sanic_ext, "Extend")
|
Ext = getattr(sanic_ext, "Extend")
|
||||||
app._ext = Ext(app, **kwargs)
|
app._ext = Ext(app, **kwargs)
|
||||||
|
|
||||||
return app.ext
|
return app.ext
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ FULL_COLOR_LOGO = """
|
|||||||
|
|
||||||
""" # noqa
|
""" # noqa
|
||||||
|
|
||||||
|
SVG_LOGO_SIMPLE = """<svg id=logo-simple viewBox="0 0 964 279"><desc>Sanic</desc><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#1f1f1f"/></svg>""" # noqa
|
||||||
|
|
||||||
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,20 +80,23 @@ class MOTDTTY(MOTD):
|
|||||||
)
|
)
|
||||||
self.display_length = self.key_width + self.value_width + 2
|
self.display_length = self.key_width + self.value_width + 2
|
||||||
|
|
||||||
def display(self):
|
def display(self, version=True, action="Goin' Fast", out=None):
|
||||||
version = f"Sanic v{__version__}".center(self.centering_length)
|
if not out:
|
||||||
|
out = logger.info
|
||||||
|
header = "Sanic"
|
||||||
|
if version:
|
||||||
|
header += f" v{__version__}"
|
||||||
|
header = header.center(self.centering_length)
|
||||||
running = (
|
running = (
|
||||||
f"Goin' Fast @ {self.serve_location}"
|
f"{action} @ {self.serve_location}" if self.serve_location else ""
|
||||||
if self.serve_location
|
|
||||||
else ""
|
|
||||||
).center(self.centering_length)
|
).center(self.centering_length)
|
||||||
length = len(version) + 2 - self.logo_line_length
|
length = len(header) + 2 - self.logo_line_length
|
||||||
first_filler = "─" * (self.logo_line_length - 1)
|
first_filler = "─" * (self.logo_line_length - 1)
|
||||||
second_filler = "─" * length
|
second_filler = "─" * length
|
||||||
display_filler = "─" * (self.display_length + 2)
|
display_filler = "─" * (self.display_length + 2)
|
||||||
lines = [
|
lines = [
|
||||||
f"\n┌{first_filler}─{second_filler}┐",
|
f"\n┌{first_filler}─{second_filler}┐",
|
||||||
f"│ {version} │",
|
f"│ {header} │",
|
||||||
f"│ {running} │",
|
f"│ {running} │",
|
||||||
f"├{first_filler}┬{second_filler}┤",
|
f"├{first_filler}┬{second_filler}┤",
|
||||||
]
|
]
|
||||||
@@ -107,7 +110,7 @@ class MOTDTTY(MOTD):
|
|||||||
self._render_fill(lines)
|
self._render_fill(lines)
|
||||||
|
|
||||||
lines.append(f"└{first_filler}┴{second_filler}┘\n")
|
lines.append(f"└{first_filler}┴{second_filler}┘\n")
|
||||||
logger.info(indent("\n".join(lines), " "))
|
out(indent("\n".join(lines), " "))
|
||||||
|
|
||||||
def _render_data(self, lines, data, start):
|
def _render_data(self, lines, data, start):
|
||||||
offset = 0
|
offset = 0
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ from urllib.parse import quote
|
|||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.helpers import _default
|
from sanic.helpers import Default
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
from sanic.log import logger
|
from sanic.log import error_logger, logger
|
||||||
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import BaseHTTPResponse
|
from sanic.response import BaseHTTPResponse
|
||||||
@@ -61,7 +61,7 @@ class Lifespan:
|
|||||||
await self.asgi_app.sanic_app._server_event("init", "before")
|
await self.asgi_app.sanic_app._server_event("init", "before")
|
||||||
await self.asgi_app.sanic_app._server_event("init", "after")
|
await self.asgi_app.sanic_app._server_event("init", "after")
|
||||||
|
|
||||||
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
|
if not isinstance(self.asgi_app.sanic_app.config.USE_UVLOOP, Default):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"You have set the USE_UVLOOP configuration option, but Sanic "
|
"You have set the USE_UVLOOP configuration option, but Sanic "
|
||||||
"cannot control the event loop when running in ASGI mode."
|
"cannot control the event loop when running in ASGI mode."
|
||||||
@@ -85,13 +85,27 @@ class Lifespan:
|
|||||||
) -> None:
|
) -> None:
|
||||||
message = await receive()
|
message = await receive()
|
||||||
if message["type"] == "lifespan.startup":
|
if message["type"] == "lifespan.startup":
|
||||||
await self.startup()
|
try:
|
||||||
await send({"type": "lifespan.startup.complete"})
|
await self.startup()
|
||||||
|
except Exception as e:
|
||||||
|
error_logger.exception(e)
|
||||||
|
await send(
|
||||||
|
{"type": "lifespan.startup.failed", "message": str(e)}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await send({"type": "lifespan.startup.complete"})
|
||||||
|
|
||||||
message = await receive()
|
message = await receive()
|
||||||
if message["type"] == "lifespan.shutdown":
|
if message["type"] == "lifespan.shutdown":
|
||||||
await self.shutdown()
|
try:
|
||||||
await send({"type": "lifespan.shutdown.complete"})
|
await self.shutdown()
|
||||||
|
except Exception as e:
|
||||||
|
error_logger.exception(e)
|
||||||
|
await send(
|
||||||
|
{"type": "lifespan.shutdown.failed", "message": str(e)}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await send({"type": "lifespan.shutdown.complete"})
|
||||||
|
|
||||||
|
|
||||||
class ASGIApp:
|
class ASGIApp:
|
||||||
@@ -234,4 +248,7 @@ class ASGIApp:
|
|||||||
self.stage = Stage.HANDLER
|
self.stage = Stage.HANDLER
|
||||||
await self.sanic_app.handle_request(self.request)
|
await self.sanic_app.handle_request(self.request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.sanic_app.handle_exception(self.request, e)
|
try:
|
||||||
|
await self.sanic_app.handle_exception(self.request, e)
|
||||||
|
except Exception as exc:
|
||||||
|
await self.sanic_app.handle_exception(self.request, exc, False)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
@@ -9,6 +9,7 @@ from sanic.mixins.listeners import ListenerMixin
|
|||||||
from sanic.mixins.middleware import MiddlewareMixin
|
from sanic.mixins.middleware import MiddlewareMixin
|
||||||
from sanic.mixins.routes import RouteMixin
|
from sanic.mixins.routes import RouteMixin
|
||||||
from sanic.mixins.signals import SignalMixin
|
from sanic.mixins.signals import SignalMixin
|
||||||
|
from sanic.mixins.static import StaticMixin
|
||||||
|
|
||||||
|
|
||||||
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
|
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
|
||||||
@@ -16,6 +17,7 @@ VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
|
|||||||
|
|
||||||
class BaseSanic(
|
class BaseSanic(
|
||||||
RouteMixin,
|
RouteMixin,
|
||||||
|
StaticMixin,
|
||||||
MiddlewareMixin,
|
MiddlewareMixin,
|
||||||
ListenerMixin,
|
ListenerMixin,
|
||||||
ExceptionMixin,
|
ExceptionMixin,
|
||||||
@@ -24,7 +26,9 @@ class BaseSanic(
|
|||||||
):
|
):
|
||||||
__slots__ = ("name",)
|
__slots__ = ("name",)
|
||||||
|
|
||||||
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
|
def __init__(
|
||||||
|
self, name: Optional[str] = None, *args: Any, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class Blueprint(BaseSanic):
|
|||||||
"version",
|
"version",
|
||||||
"version_prefix",
|
"version_prefix",
|
||||||
"websocket_routes",
|
"websocket_routes",
|
||||||
|
"wrappers",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -304,11 +305,8 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
# Routes
|
# Routes
|
||||||
for future in self._future_routes:
|
for future in self._future_routes:
|
||||||
# attach the blueprint name to the handler so that it can be
|
|
||||||
# prefixed properly in the router
|
|
||||||
future.handler.__blueprintname__ = self.name
|
|
||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
uri = self._setup_uri(future.uri, url_prefix)
|
||||||
|
|
||||||
version_prefix = self.version_prefix
|
version_prefix = self.version_prefix
|
||||||
for prefix in (
|
for prefix in (
|
||||||
@@ -333,7 +331,7 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
apply_route = FutureRoute(
|
apply_route = FutureRoute(
|
||||||
future.handler,
|
future.handler,
|
||||||
uri[1:] if uri.startswith("//") else uri,
|
uri,
|
||||||
future.methods,
|
future.methods,
|
||||||
host,
|
host,
|
||||||
strict_slashes,
|
strict_slashes,
|
||||||
@@ -363,7 +361,7 @@ class Blueprint(BaseSanic):
|
|||||||
# Static Files
|
# Static Files
|
||||||
for future in self._future_statics:
|
for future in self._future_statics:
|
||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
uri = self._setup_uri(future.uri, url_prefix)
|
||||||
apply_route = FutureStatic(uri, *future[1:])
|
apply_route = FutureStatic(uri, *future[1:])
|
||||||
|
|
||||||
if (self, apply_route) in app._future_registry:
|
if (self, apply_route) in app._future_registry:
|
||||||
@@ -406,7 +404,7 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||||
self.websocket_routes += [
|
self.websocket_routes += [
|
||||||
route for route in self.routes if route.ctx.websocket
|
route for route in self.routes if route.extra.websocket
|
||||||
]
|
]
|
||||||
self.middlewares += middleware
|
self.middlewares += middleware
|
||||||
self.exceptions += exception_handlers
|
self.exceptions += exception_handlers
|
||||||
@@ -442,7 +440,7 @@ class Blueprint(BaseSanic):
|
|||||||
events.add(signal.ctx.event)
|
events.add(signal.ctx.event)
|
||||||
|
|
||||||
return asyncio.wait(
|
return asyncio.wait(
|
||||||
[event.wait() for event in events],
|
[asyncio.create_task(event.wait()) for event in events],
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
@@ -456,6 +454,18 @@ class Blueprint(BaseSanic):
|
|||||||
break
|
break
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _setup_uri(base: str, prefix: Optional[str]):
|
||||||
|
uri = base
|
||||||
|
if prefix:
|
||||||
|
uri = prefix
|
||||||
|
if base.startswith("/") and prefix.endswith("/"):
|
||||||
|
uri += base[1:]
|
||||||
|
else:
|
||||||
|
uri += base
|
||||||
|
|
||||||
|
return uri[1:] if uri.startswith("//") else uri
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register_futures(
|
def register_futures(
|
||||||
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
|
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
|
||||||
|
|||||||
186
sanic/cli/app.py
186
sanic/cli/app.py
@@ -1,22 +1,21 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
from argparse import Namespace
|
||||||
from importlib import import_module
|
from functools import partial
|
||||||
from pathlib import Path
|
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
from typing import Any, List, Union
|
from typing import List, Union, cast
|
||||||
|
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.application.logo import get_logo
|
from sanic.application.logo import get_logo
|
||||||
from sanic.cli.arguments import Group
|
from sanic.cli.arguments import Group
|
||||||
from sanic.log import error_logger
|
from sanic.cli.base import SanicArgumentParser, SanicHelpFormatter
|
||||||
from sanic.simple import create_simple_server
|
from sanic.cli.inspector import make_inspector_parser
|
||||||
|
from sanic.cli.inspector_client import InspectorClient
|
||||||
|
from sanic.log import Colors, error_logger
|
||||||
class SanicArgumentParser(ArgumentParser):
|
from sanic.worker.loader import AppLoader
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class SanicCLI:
|
class SanicCLI:
|
||||||
@@ -45,7 +44,7 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
self.parser = SanicArgumentParser(
|
self.parser = SanicArgumentParser(
|
||||||
prog="sanic",
|
prog="sanic",
|
||||||
description=self.DESCRIPTION,
|
description=self.DESCRIPTION,
|
||||||
formatter_class=lambda prog: RawTextHelpFormatter(
|
formatter_class=lambda prog: SanicHelpFormatter(
|
||||||
prog,
|
prog,
|
||||||
max_help_position=36 if width > 96 else 24,
|
max_help_position=36 if width > 96 else 24,
|
||||||
indent_increment=4,
|
indent_increment=4,
|
||||||
@@ -57,22 +56,37 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
self.main_process = (
|
self.main_process = (
|
||||||
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
|
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
|
||||||
)
|
)
|
||||||
self.args: List[Any] = []
|
self.args: Namespace = Namespace()
|
||||||
self.groups: List[Group] = []
|
self.groups: List[Group] = []
|
||||||
|
self.inspecting = False
|
||||||
|
|
||||||
def attach(self):
|
def attach(self):
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "inspect":
|
||||||
|
self.inspecting = True
|
||||||
|
self.parser.description = get_logo(True)
|
||||||
|
make_inspector_parser(self.parser)
|
||||||
|
return
|
||||||
|
|
||||||
for group in Group._registry:
|
for group in Group._registry:
|
||||||
instance = group.create(self.parser)
|
instance = group.create(self.parser)
|
||||||
instance.attach()
|
instance.attach()
|
||||||
self.groups.append(instance)
|
self.groups.append(instance)
|
||||||
|
|
||||||
def run(self):
|
def run(self, parse_args=None):
|
||||||
# This is to provide backwards compat -v to display version
|
if self.inspecting:
|
||||||
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
|
self._inspector()
|
||||||
parse_args = ["--version"] if legacy_version else None
|
return
|
||||||
|
|
||||||
|
legacy_version = False
|
||||||
if not parse_args:
|
if not parse_args:
|
||||||
parsed, unknown = self.parser.parse_known_args()
|
# This is to provide backwards compat -v to display version
|
||||||
|
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
|
||||||
|
parse_args = ["--version"] if legacy_version else None
|
||||||
|
elif parse_args == ["-v"]:
|
||||||
|
parse_args = ["--version"]
|
||||||
|
|
||||||
|
if not legacy_version:
|
||||||
|
parsed, unknown = self.parser.parse_known_args(args=parse_args)
|
||||||
if unknown and parsed.factory:
|
if unknown and parsed.factory:
|
||||||
for arg in unknown:
|
for arg in unknown:
|
||||||
if arg.startswith("--"):
|
if arg.startswith("--"):
|
||||||
@@ -80,20 +94,90 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
|
|
||||||
self.args = self.parser.parse_args(args=parse_args)
|
self.args = self.parser.parse_args(args=parse_args)
|
||||||
self._precheck()
|
self._precheck()
|
||||||
|
app_loader = AppLoader(
|
||||||
|
self.args.module, self.args.factory, self.args.simple, self.args
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
|
||||||
|
self._inspector_legacy(app_loader)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = self._get_app()
|
app = self._get_app(app_loader)
|
||||||
kwargs = self._build_run_kwargs()
|
kwargs = self._build_run_kwargs()
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
error_logger.exception("Failed to run app")
|
error_logger.exception(f"Failed to run app: {e}")
|
||||||
else:
|
else:
|
||||||
for http_version in self.args.http:
|
for http_version in self.args.http:
|
||||||
app.prepare(**kwargs, version=http_version)
|
app.prepare(**kwargs, version=http_version)
|
||||||
|
if self.args.single:
|
||||||
|
serve = Sanic.serve_single
|
||||||
|
elif self.args.legacy:
|
||||||
|
serve = Sanic.serve_legacy
|
||||||
|
else:
|
||||||
|
serve = partial(Sanic.serve, app_loader=app_loader)
|
||||||
|
serve(app)
|
||||||
|
|
||||||
Sanic.serve()
|
def _inspector_legacy(self, app_loader: AppLoader):
|
||||||
|
host = port = None
|
||||||
|
module = cast(str, self.args.module)
|
||||||
|
if ":" in module:
|
||||||
|
maybe_host, maybe_port = module.rsplit(":", 1)
|
||||||
|
if maybe_port.isnumeric():
|
||||||
|
host, port = maybe_host, int(maybe_port)
|
||||||
|
if not host:
|
||||||
|
app = self._get_app(app_loader)
|
||||||
|
host, port = app.config.INSPECTOR_HOST, app.config.INSPECTOR_PORT
|
||||||
|
|
||||||
|
action = self.args.trigger or "info"
|
||||||
|
|
||||||
|
InspectorClient(
|
||||||
|
str(host), int(port or 6457), False, self.args.inspect_raw, ""
|
||||||
|
).do(action)
|
||||||
|
sys.stdout.write(
|
||||||
|
f"\n{Colors.BOLD}{Colors.YELLOW}WARNING:{Colors.END} "
|
||||||
|
"You are using the legacy CLI command that will be removed in "
|
||||||
|
f"{Colors.RED}v23.3{Colors.END}. See "
|
||||||
|
"https://sanic.dev/en/guide/release-notes/v22.12.html"
|
||||||
|
"#deprecations-and-removals or checkout the new "
|
||||||
|
"style commands:\n\n\t"
|
||||||
|
f"{Colors.YELLOW}sanic inspect --help{Colors.END}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _inspector(self):
|
||||||
|
args = sys.argv[2:]
|
||||||
|
self.args, unknown = self.parser.parse_known_args(args=args)
|
||||||
|
if unknown:
|
||||||
|
for arg in unknown:
|
||||||
|
if arg.startswith("--"):
|
||||||
|
try:
|
||||||
|
key, value = arg.split("=")
|
||||||
|
key = key.lstrip("-")
|
||||||
|
except ValueError:
|
||||||
|
value = False if arg.startswith("--no-") else True
|
||||||
|
key = (
|
||||||
|
arg.replace("--no-", "")
|
||||||
|
.lstrip("-")
|
||||||
|
.replace("-", "_")
|
||||||
|
)
|
||||||
|
setattr(self.args, key, value)
|
||||||
|
|
||||||
|
kwargs = {**self.args.__dict__}
|
||||||
|
host = kwargs.pop("host")
|
||||||
|
port = kwargs.pop("port")
|
||||||
|
secure = kwargs.pop("secure")
|
||||||
|
raw = kwargs.pop("raw")
|
||||||
|
action = kwargs.pop("action") or "info"
|
||||||
|
api_key = kwargs.pop("api_key")
|
||||||
|
positional = kwargs.pop("positional", None)
|
||||||
|
if action == "<custom>" and positional:
|
||||||
|
action = positional[0]
|
||||||
|
if len(positional) > 1:
|
||||||
|
kwargs["args"] = positional[1:]
|
||||||
|
InspectorClient(host, port, secure, raw, api_key).do(action, **kwargs)
|
||||||
|
|
||||||
def _precheck(self):
|
def _precheck(self):
|
||||||
# # Custom TLS mismatch handling for better diagnostics
|
# Custom TLS mismatch handling for better diagnostics
|
||||||
if self.main_process and (
|
if self.main_process and (
|
||||||
# one of cert/key missing
|
# one of cert/key missing
|
||||||
bool(self.args.cert) != bool(self.args.key)
|
bool(self.args.cert) != bool(self.args.key)
|
||||||
@@ -113,58 +197,14 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
)
|
)
|
||||||
error_logger.error(message)
|
error_logger.error(message)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if self.args.inspect or self.args.inspect_raw:
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
def _get_app(self):
|
def _get_app(self, app_loader: AppLoader):
|
||||||
try:
|
try:
|
||||||
module_path = os.path.abspath(os.getcwd())
|
app = app_loader.load()
|
||||||
if module_path not in sys.path:
|
|
||||||
sys.path.append(module_path)
|
|
||||||
|
|
||||||
if self.args.simple:
|
|
||||||
path = Path(self.args.module)
|
|
||||||
app = create_simple_server(path)
|
|
||||||
else:
|
|
||||||
delimiter = ":" if ":" in self.args.module else "."
|
|
||||||
module_name, app_name = self.args.module.rsplit(delimiter, 1)
|
|
||||||
|
|
||||||
if module_name == "" and os.path.isdir(self.args.module):
|
|
||||||
raise ValueError(
|
|
||||||
"App not found.\n"
|
|
||||||
" Please use --simple if you are passing a "
|
|
||||||
"directory to sanic.\n"
|
|
||||||
f" eg. sanic {self.args.module} --simple"
|
|
||||||
)
|
|
||||||
|
|
||||||
if app_name.endswith("()"):
|
|
||||||
self.args.factory = True
|
|
||||||
app_name = app_name[:-2]
|
|
||||||
|
|
||||||
module = import_module(module_name)
|
|
||||||
app = getattr(module, app_name, None)
|
|
||||||
if self.args.factory:
|
|
||||||
try:
|
|
||||||
app = app(self.args)
|
|
||||||
except TypeError:
|
|
||||||
app = app()
|
|
||||||
|
|
||||||
app_type_name = type(app).__name__
|
|
||||||
|
|
||||||
if not isinstance(app, Sanic):
|
|
||||||
if callable(app):
|
|
||||||
solution = f"sanic {self.args.module} --factory"
|
|
||||||
raise ValueError(
|
|
||||||
"Module is not a Sanic app, it is a "
|
|
||||||
f"{app_type_name}\n"
|
|
||||||
" If this callable returns a "
|
|
||||||
f"Sanic instance try: \n{solution}"
|
|
||||||
)
|
|
||||||
|
|
||||||
raise ValueError(
|
|
||||||
f"Module is not a Sanic app, it is a {app_type_name}\n"
|
|
||||||
f" Perhaps you meant {self.args.module}:app?"
|
|
||||||
)
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
if module_name.startswith(e.name):
|
if app_loader.module_name.startswith(e.name): # type: ignore
|
||||||
error_logger.error(
|
error_logger.error(
|
||||||
f"No module named {e.name} found.\n"
|
f"No module named {e.name} found.\n"
|
||||||
" Example File: project/sanic_server.py -> app\n"
|
" Example File: project/sanic_server.py -> app\n"
|
||||||
@@ -190,8 +230,10 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
elif len(ssl) == 1 and ssl[0] is not None:
|
elif len(ssl) == 1 and ssl[0] is not None:
|
||||||
# Use only one cert, no TLSSelector.
|
# Use only one cert, no TLSSelector.
|
||||||
ssl = ssl[0]
|
ssl = ssl[0]
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"access_log": self.args.access_log,
|
"access_log": self.args.access_log,
|
||||||
|
"coffee": self.args.coffee,
|
||||||
"debug": self.args.debug,
|
"debug": self.args.debug,
|
||||||
"fast": self.args.fast,
|
"fast": self.args.fast,
|
||||||
"host": self.args.host,
|
"host": self.args.host,
|
||||||
@@ -203,6 +245,8 @@ Or, a path to a directory to run as a simple HTTP server:
|
|||||||
"verbosity": self.args.verbosity or 0,
|
"verbosity": self.args.verbosity or 0,
|
||||||
"workers": self.args.workers,
|
"workers": self.args.workers,
|
||||||
"auto_tls": self.args.auto_tls,
|
"auto_tls": self.args.auto_tls,
|
||||||
|
"single_process": self.args.single,
|
||||||
|
"legacy": self.args.legacy,
|
||||||
}
|
}
|
||||||
|
|
||||||
for maybe_arg in ("auto_reload", "dev"):
|
for maybe_arg in ("auto_reload", "dev"):
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Group:
|
|||||||
instance = cls(parser, cls.name)
|
instance = cls(parser, cls.name)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def add_bool_arguments(self, *args, **kwargs):
|
def add_bool_arguments(self, *args, nullable=False, **kwargs):
|
||||||
group = self.container.add_mutually_exclusive_group()
|
group = self.container.add_mutually_exclusive_group()
|
||||||
kwargs["help"] = kwargs["help"].capitalize()
|
kwargs["help"] = kwargs["help"].capitalize()
|
||||||
group.add_argument(*args, action="store_true", **kwargs)
|
group.add_argument(*args, action="store_true", **kwargs)
|
||||||
@@ -38,6 +38,9 @@ class Group:
|
|||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||||
)
|
)
|
||||||
|
if nullable:
|
||||||
|
params = {args[0][2:].replace("-", "_"): None}
|
||||||
|
group.set_defaults(**params)
|
||||||
|
|
||||||
def prepare(self, args) -> None:
|
def prepare(self, args) -> None:
|
||||||
...
|
...
|
||||||
@@ -67,7 +70,8 @@ class ApplicationGroup(Group):
|
|||||||
name = "Application"
|
name = "Application"
|
||||||
|
|
||||||
def attach(self):
|
def attach(self):
|
||||||
self.container.add_argument(
|
group = self.container.add_mutually_exclusive_group()
|
||||||
|
group.add_argument(
|
||||||
"--factory",
|
"--factory",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help=(
|
help=(
|
||||||
@@ -75,7 +79,7 @@ class ApplicationGroup(Group):
|
|||||||
"i.e. a () -> <Sanic app> callable"
|
"i.e. a () -> <Sanic app> callable"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.container.add_argument(
|
group.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
"--simple",
|
"--simple",
|
||||||
dest="simple",
|
dest="simple",
|
||||||
@@ -85,6 +89,32 @@ class ApplicationGroup(Group):
|
|||||||
"a directory\n(module arg should be a path)"
|
"a directory\n(module arg should be a path)"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--inspect",
|
||||||
|
dest="inspect",
|
||||||
|
action="store_true",
|
||||||
|
help=("Inspect the state of a running instance, human readable"),
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--inspect-raw",
|
||||||
|
dest="inspect_raw",
|
||||||
|
action="store_true",
|
||||||
|
help=("Inspect the state of a running instance, JSON output"),
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--trigger-reload",
|
||||||
|
dest="trigger",
|
||||||
|
action="store_const",
|
||||||
|
const="reload",
|
||||||
|
help=("Trigger worker processes to reload"),
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--trigger-shutdown",
|
||||||
|
dest="trigger",
|
||||||
|
action="store_const",
|
||||||
|
const="shutdown",
|
||||||
|
help=("Trigger all processes to shutdown"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HTTPVersionGroup(Group):
|
class HTTPVersionGroup(Group):
|
||||||
@@ -207,8 +237,22 @@ class WorkerGroup(Group):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Set the number of workers to max allowed",
|
help="Set the number of workers to max allowed",
|
||||||
)
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--single-process",
|
||||||
|
dest="single",
|
||||||
|
action="store_true",
|
||||||
|
help="Do not use multiprocessing, run server in a single process",
|
||||||
|
)
|
||||||
|
self.container.add_argument(
|
||||||
|
"--legacy",
|
||||||
|
action="store_true",
|
||||||
|
help="Use the legacy server manager",
|
||||||
|
)
|
||||||
self.add_bool_arguments(
|
self.add_bool_arguments(
|
||||||
"--access-logs", dest="access_log", help="display access logs"
|
"--access-logs",
|
||||||
|
dest="access_log",
|
||||||
|
help="display access logs",
|
||||||
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -262,6 +306,12 @@ class OutputGroup(Group):
|
|||||||
name = "Output"
|
name = "Output"
|
||||||
|
|
||||||
def attach(self):
|
def attach(self):
|
||||||
|
self.add_bool_arguments(
|
||||||
|
"--coffee",
|
||||||
|
dest="coffee",
|
||||||
|
default=False,
|
||||||
|
help="Uhm, coffee?",
|
||||||
|
)
|
||||||
self.add_bool_arguments(
|
self.add_bool_arguments(
|
||||||
"--motd",
|
"--motd",
|
||||||
dest="motd",
|
dest="motd",
|
||||||
|
|||||||
35
sanic/cli/base.py
Normal file
35
sanic/cli/base.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from argparse import (
|
||||||
|
SUPPRESS,
|
||||||
|
Action,
|
||||||
|
ArgumentParser,
|
||||||
|
RawTextHelpFormatter,
|
||||||
|
_SubParsersAction,
|
||||||
|
)
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class SanicArgumentParser(ArgumentParser):
|
||||||
|
def _check_value(self, action: Action, value: Any) -> None:
|
||||||
|
if isinstance(action, SanicSubParsersAction):
|
||||||
|
return
|
||||||
|
super()._check_value(action, value)
|
||||||
|
|
||||||
|
|
||||||
|
class SanicHelpFormatter(RawTextHelpFormatter):
|
||||||
|
def add_usage(self, usage, actions, groups, prefix=None):
|
||||||
|
if not usage:
|
||||||
|
usage = SUPPRESS
|
||||||
|
# Add one linebreak, but not two
|
||||||
|
self.add_text("\x1b[1A")
|
||||||
|
super().add_usage(usage, actions, groups, prefix)
|
||||||
|
|
||||||
|
|
||||||
|
class SanicSubParsersAction(_SubParsersAction):
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
self._name_parser_map
|
||||||
|
parser_name = values[0]
|
||||||
|
if parser_name not in self._name_parser_map:
|
||||||
|
self._name_parser_map[parser_name] = parser
|
||||||
|
values = ["<custom>", *values]
|
||||||
|
|
||||||
|
super().__call__(parser, namespace, values, option_string)
|
||||||
105
sanic/cli/inspector.py
Normal file
105
sanic/cli/inspector.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
from sanic.application.logo import get_logo
|
||||||
|
from sanic.cli.base import SanicHelpFormatter, SanicSubParsersAction
|
||||||
|
|
||||||
|
|
||||||
|
def _add_shared(parser: ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
"-H",
|
||||||
|
default="localhost",
|
||||||
|
help="Inspector host address [default 127.0.0.1]",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
"-p",
|
||||||
|
default=6457,
|
||||||
|
type=int,
|
||||||
|
help="Inspector port [default 6457]",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--secure",
|
||||||
|
"-s",
|
||||||
|
action="store_true",
|
||||||
|
help="Whether to access the Inspector via TLS encryption",
|
||||||
|
)
|
||||||
|
parser.add_argument("--api-key", "-k", help="Inspector authentication key")
|
||||||
|
parser.add_argument(
|
||||||
|
"--raw",
|
||||||
|
action="store_true",
|
||||||
|
help="Whether to output the raw response information",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InspectorSubParser(ArgumentParser):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
_add_shared(self)
|
||||||
|
if not self.description:
|
||||||
|
self.description = ""
|
||||||
|
self.description = get_logo(True) + self.description
|
||||||
|
|
||||||
|
|
||||||
|
def make_inspector_parser(parser: ArgumentParser) -> None:
|
||||||
|
_add_shared(parser)
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
action=SanicSubParsersAction,
|
||||||
|
dest="action",
|
||||||
|
description=(
|
||||||
|
"Run one or none of the below subcommands. Using inspect without "
|
||||||
|
"a subcommand will fetch general information about the state "
|
||||||
|
"of the application instance.\n\n"
|
||||||
|
"Or, you can optionally follow inspect with a subcommand. "
|
||||||
|
"If you have created a custom "
|
||||||
|
"Inspector instance, then you can run custom commands. See "
|
||||||
|
"https://sanic.dev/en/guide/deployment/inspector.html "
|
||||||
|
"for more details."
|
||||||
|
),
|
||||||
|
title=" Subcommands",
|
||||||
|
parser_class=InspectorSubParser,
|
||||||
|
)
|
||||||
|
reloader = subparsers.add_parser(
|
||||||
|
"reload",
|
||||||
|
help="Trigger a reload of the server workers",
|
||||||
|
formatter_class=SanicHelpFormatter,
|
||||||
|
)
|
||||||
|
reloader.add_argument(
|
||||||
|
"--zero-downtime",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Whether to wait for the new process to be online before "
|
||||||
|
"terminating the old"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
subparsers.add_parser(
|
||||||
|
"shutdown",
|
||||||
|
help="Shutdown the application and all processes",
|
||||||
|
formatter_class=SanicHelpFormatter,
|
||||||
|
)
|
||||||
|
scale = subparsers.add_parser(
|
||||||
|
"scale",
|
||||||
|
help="Scale the number of workers",
|
||||||
|
formatter_class=SanicHelpFormatter,
|
||||||
|
)
|
||||||
|
scale.add_argument(
|
||||||
|
"replicas",
|
||||||
|
type=int,
|
||||||
|
help="Number of workers requested",
|
||||||
|
)
|
||||||
|
|
||||||
|
custom = subparsers.add_parser(
|
||||||
|
"<custom>",
|
||||||
|
help="Run a custom command",
|
||||||
|
description=(
|
||||||
|
"keyword arguments:\n When running a custom command, you can "
|
||||||
|
"add keyword arguments by appending them to your command\n\n"
|
||||||
|
"\tsanic inspect foo --one=1 --two=2"
|
||||||
|
),
|
||||||
|
formatter_class=SanicHelpFormatter,
|
||||||
|
)
|
||||||
|
custom.add_argument(
|
||||||
|
"positional",
|
||||||
|
nargs="*",
|
||||||
|
help="Add one or more non-keyword args to your custom command",
|
||||||
|
)
|
||||||
119
sanic/cli/inspector_client.py
Normal file
119
sanic/cli/inspector_client.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from http.client import RemoteDisconnected
|
||||||
|
from textwrap import indent
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from urllib.error import URLError
|
||||||
|
from urllib.request import Request as URequest
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
from sanic.application.logo import get_logo
|
||||||
|
from sanic.application.motd import MOTDTTY
|
||||||
|
from sanic.log import Colors
|
||||||
|
|
||||||
|
|
||||||
|
try: # no cov
|
||||||
|
from ujson import dumps, loads
|
||||||
|
except ModuleNotFoundError: # no cov
|
||||||
|
from json import dumps, loads # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class InspectorClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
secure: bool,
|
||||||
|
raw: bool,
|
||||||
|
api_key: Optional[str],
|
||||||
|
) -> None:
|
||||||
|
self.scheme = "https" if secure else "http"
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.raw = raw
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
|
for scheme in ("http", "https"):
|
||||||
|
full = f"{scheme}://"
|
||||||
|
if self.host.startswith(full):
|
||||||
|
self.scheme = scheme
|
||||||
|
self.host = self.host[len(full) :] # noqa E203
|
||||||
|
|
||||||
|
def do(self, action: str, **kwargs: Any) -> None:
|
||||||
|
if action == "info":
|
||||||
|
self.info()
|
||||||
|
return
|
||||||
|
result = self.request(action, **kwargs).get("result")
|
||||||
|
if result:
|
||||||
|
out = (
|
||||||
|
dumps(result)
|
||||||
|
if isinstance(result, (list, dict))
|
||||||
|
else str(result)
|
||||||
|
)
|
||||||
|
sys.stdout.write(out + "\n")
|
||||||
|
|
||||||
|
def info(self) -> None:
|
||||||
|
out = sys.stdout.write
|
||||||
|
response = self.request("", "GET")
|
||||||
|
if self.raw or not response:
|
||||||
|
return
|
||||||
|
data = response["result"]
|
||||||
|
display = data.pop("info")
|
||||||
|
extra = display.pop("extra", {})
|
||||||
|
display["packages"] = ", ".join(display["packages"])
|
||||||
|
MOTDTTY(get_logo(), self.base_url, display, extra).display(
|
||||||
|
version=False,
|
||||||
|
action="Inspecting",
|
||||||
|
out=out,
|
||||||
|
)
|
||||||
|
for name, info in data["workers"].items():
|
||||||
|
info = "\n".join(
|
||||||
|
f"\t{key}: {Colors.BLUE}{value}{Colors.END}"
|
||||||
|
for key, value in info.items()
|
||||||
|
)
|
||||||
|
out(
|
||||||
|
"\n"
|
||||||
|
+ indent(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
f"{Colors.BOLD}{Colors.SANIC}{name}{Colors.END}",
|
||||||
|
info,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
" ",
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any:
|
||||||
|
url = f"{self.base_url}/{action}"
|
||||||
|
params: Dict[str, Any] = {"method": method, "headers": {}}
|
||||||
|
if kwargs:
|
||||||
|
params["data"] = dumps(kwargs).encode()
|
||||||
|
params["headers"]["content-type"] = "application/json"
|
||||||
|
if self.api_key:
|
||||||
|
params["headers"]["authorization"] = f"Bearer {self.api_key}"
|
||||||
|
request = URequest(url, **params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urlopen(request) as response: # nosec B310
|
||||||
|
raw = response.read()
|
||||||
|
loaded = loads(raw)
|
||||||
|
if self.raw:
|
||||||
|
sys.stdout.write(dumps(loaded.get("result")) + "\n")
|
||||||
|
return {}
|
||||||
|
return loaded
|
||||||
|
except (URLError, RemoteDisconnected) as e:
|
||||||
|
sys.stderr.write(
|
||||||
|
f"{Colors.RED}Could not connect to inspector at: "
|
||||||
|
f"{Colors.YELLOW}{self.base_url}{Colors.END}\n"
|
||||||
|
"Either the application is not running, or it did not start "
|
||||||
|
f"an inspector instance.\n{e}\n"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self):
|
||||||
|
return f"{self.scheme}://{self.host}:{self.port}"
|
||||||
@@ -3,10 +3,23 @@ import os
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from typing import Awaitable
|
from contextlib import contextmanager
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Awaitable, Union
|
||||||
|
|
||||||
from multidict import CIMultiDict # type: ignore
|
from multidict import CIMultiDict # type: ignore
|
||||||
|
|
||||||
|
from sanic.helpers import Default
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 8): # no cov
|
||||||
|
StartMethod = Union[Default, str]
|
||||||
|
else: # no cov
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
StartMethod = Union[
|
||||||
|
Default, Literal["fork"], Literal["forkserver"], Literal["spawn"]
|
||||||
|
]
|
||||||
|
|
||||||
OS_IS_WINDOWS = os.name == "nt"
|
OS_IS_WINDOWS = os.name == "nt"
|
||||||
UVLOOP_INSTALLED = False
|
UVLOOP_INSTALLED = False
|
||||||
@@ -18,6 +31,40 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Python 3.11 changed the way Enum formatting works for mixed-in types.
|
||||||
|
if sys.version_info < (3, 11, 0):
|
||||||
|
|
||||||
|
class StrEnum(str, Enum):
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
from enum import StrEnum # type: ignore # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class UpperStrEnum(StrEnum):
|
||||||
|
def _generate_next_value_(name, start, count, last_values):
|
||||||
|
return name.upper()
|
||||||
|
|
||||||
|
def __eq__(self, value: object) -> bool:
|
||||||
|
value = str(value).upper()
|
||||||
|
return super().__eq__(value)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.value)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def use_context(method: StartMethod):
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
orig = Sanic.start_method
|
||||||
|
Sanic.start_method = method
|
||||||
|
yield
|
||||||
|
Sanic.start_method = orig
|
||||||
|
|
||||||
|
|
||||||
def enable_windows_color_support():
|
def enable_windows_color_support():
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|||||||
@@ -1,30 +1,55 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from abc import ABCMeta
|
||||||
from inspect import getmembers, isclass, isdatadescriptor
|
from inspect import getmembers, isclass, isdatadescriptor
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
||||||
|
from warnings import filterwarnings
|
||||||
|
|
||||||
from sanic.constants import LocalCertCreator
|
from sanic.constants import LocalCertCreator
|
||||||
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
||||||
from sanic.helpers import Default, _default
|
from sanic.helpers import Default, _default
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.utils import load_module_from_file_location, str_to_bool
|
from sanic.utils import load_module_from_file_location, str_to_bool
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
FilterWarningType = Union[
|
||||||
|
Literal["default"],
|
||||||
|
Literal["error"],
|
||||||
|
Literal["ignore"],
|
||||||
|
Literal["always"],
|
||||||
|
Literal["module"],
|
||||||
|
Literal["once"],
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
FilterWarningType = str
|
||||||
|
|
||||||
SANIC_PREFIX = "SANIC_"
|
SANIC_PREFIX = "SANIC_"
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"_FALLBACK_ERROR_FORMAT": _default,
|
"_FALLBACK_ERROR_FORMAT": _default,
|
||||||
"ACCESS_LOG": True,
|
"ACCESS_LOG": False,
|
||||||
"AUTO_EXTEND": True,
|
"AUTO_EXTEND": True,
|
||||||
"AUTO_RELOAD": False,
|
"AUTO_RELOAD": False,
|
||||||
"EVENT_AUTOREGISTER": False,
|
"EVENT_AUTOREGISTER": False,
|
||||||
|
"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, # 15 sec
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||||
|
"INSPECTOR": False,
|
||||||
|
"INSPECTOR_HOST": "localhost",
|
||||||
|
"INSPECTOR_PORT": 6457,
|
||||||
|
"INSPECTOR_TLS_KEY": _default,
|
||||||
|
"INSPECTOR_TLS_CERT": _default,
|
||||||
|
"INSPECTOR_API_KEY": "",
|
||||||
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||||
"KEEP_ALIVE": True,
|
"KEEP_ALIVE": True,
|
||||||
"LOCAL_CERT_CREATOR": LocalCertCreator.AUTO,
|
"LOCAL_CERT_CREATOR": LocalCertCreator.AUTO,
|
||||||
@@ -50,12 +75,8 @@ DEFAULT_CONFIG = {
|
|||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
# These values will be removed from the Config object in v22.6 and moved
|
|
||||||
# to the application state
|
|
||||||
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
|
|
||||||
|
|
||||||
|
class DescriptorMeta(ABCMeta):
|
||||||
class DescriptorMeta(type):
|
|
||||||
def __init__(cls, *_):
|
def __init__(cls, *_):
|
||||||
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
|
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
|
||||||
|
|
||||||
@@ -69,9 +90,16 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
AUTO_EXTEND: bool
|
AUTO_EXTEND: bool
|
||||||
AUTO_RELOAD: bool
|
AUTO_RELOAD: bool
|
||||||
EVENT_AUTOREGISTER: bool
|
EVENT_AUTOREGISTER: bool
|
||||||
|
DEPRECATION_FILTER: FilterWarningType
|
||||||
FORWARDED_FOR_HEADER: str
|
FORWARDED_FOR_HEADER: str
|
||||||
FORWARDED_SECRET: Optional[str]
|
FORWARDED_SECRET: Optional[str]
|
||||||
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
||||||
|
INSPECTOR: bool
|
||||||
|
INSPECTOR_HOST: str
|
||||||
|
INSPECTOR_PORT: int
|
||||||
|
INSPECTOR_TLS_KEY: Union[Path, str, Default]
|
||||||
|
INSPECTOR_TLS_CERT: Union[Path, str, Default]
|
||||||
|
INSPECTOR_API_KEY: str
|
||||||
KEEP_ALIVE_TIMEOUT: int
|
KEEP_ALIVE_TIMEOUT: int
|
||||||
KEEP_ALIVE: bool
|
KEEP_ALIVE: bool
|
||||||
LOCAL_CERT_CREATOR: Union[str, LocalCertCreator]
|
LOCAL_CERT_CREATOR: Union[str, LocalCertCreator]
|
||||||
@@ -99,7 +127,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
|
defaults: Optional[
|
||||||
|
Dict[str, Union[str, bool, int, float, None]]
|
||||||
|
] = None,
|
||||||
env_prefix: Optional[str] = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
keep_alive: Optional[bool] = None,
|
keep_alive: Optional[bool] = None,
|
||||||
*,
|
*,
|
||||||
@@ -107,6 +137,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
):
|
):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
self._configure_warnings()
|
||||||
|
|
||||||
self._converters = [str, str_to_bool, float, int]
|
self._converters = [str, str_to_bool, float, int]
|
||||||
|
|
||||||
@@ -127,19 +158,19 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self._check_error_format()
|
self._check_error_format()
|
||||||
self._init = True
|
self._init = True
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr: Any):
|
||||||
try:
|
try:
|
||||||
return self[attr]
|
return self[attr]
|
||||||
except KeyError as ke:
|
except KeyError as ke:
|
||||||
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
raise AttributeError(f"Config has no '{ke.args[0]}'")
|
||||||
|
|
||||||
def __setattr__(self, attr, value) -> None:
|
def __setattr__(self, attr: str, value: Any) -> None:
|
||||||
self.update({attr: value})
|
self.update({attr: value})
|
||||||
|
|
||||||
def __setitem__(self, attr, value) -> None:
|
def __setitem__(self, attr: str, value: Any) -> None:
|
||||||
self.update({attr: value})
|
self.update({attr: value})
|
||||||
|
|
||||||
def update(self, *other, **kwargs) -> None:
|
def update(self, *other: Any, **kwargs: Any) -> None:
|
||||||
kwargs.update({k: v for item in other for k, v in dict(item).items()})
|
kwargs.update({k: v for item in other for k, v in dict(item).items()})
|
||||||
setters: Dict[str, Any] = {
|
setters: Dict[str, Any] = {
|
||||||
k: kwargs.pop(k)
|
k: kwargs.pop(k)
|
||||||
@@ -172,10 +203,12 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.LOCAL_CERT_CREATOR = LocalCertCreator[
|
self.LOCAL_CERT_CREATOR = LocalCertCreator[
|
||||||
self.LOCAL_CERT_CREATOR.upper()
|
self.LOCAL_CERT_CREATOR.upper()
|
||||||
]
|
]
|
||||||
|
elif attr == "DEPRECATION_FILTER":
|
||||||
|
self._configure_warnings()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def FALLBACK_ERROR_FORMAT(self) -> str:
|
def FALLBACK_ERROR_FORMAT(self) -> str:
|
||||||
if self._FALLBACK_ERROR_FORMAT is _default:
|
if isinstance(self._FALLBACK_ERROR_FORMAT, Default):
|
||||||
return DEFAULT_FORMAT
|
return DEFAULT_FORMAT
|
||||||
return self._FALLBACK_ERROR_FORMAT
|
return self._FALLBACK_ERROR_FORMAT
|
||||||
|
|
||||||
@@ -183,7 +216,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
def FALLBACK_ERROR_FORMAT(self, value):
|
def FALLBACK_ERROR_FORMAT(self, value):
|
||||||
self._check_error_format(value)
|
self._check_error_format(value)
|
||||||
if (
|
if (
|
||||||
self._FALLBACK_ERROR_FORMAT is not _default
|
not isinstance(self._FALLBACK_ERROR_FORMAT, Default)
|
||||||
and value != self._FALLBACK_ERROR_FORMAT
|
and value != self._FALLBACK_ERROR_FORMAT
|
||||||
):
|
):
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
@@ -199,6 +232,13 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.REQUEST_MAX_SIZE,
|
self.REQUEST_MAX_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _configure_warnings(self):
|
||||||
|
filterwarnings(
|
||||||
|
self.DEPRECATION_FILTER,
|
||||||
|
category=DeprecationWarning,
|
||||||
|
module=r"sanic.*",
|
||||||
|
)
|
||||||
|
|
||||||
def _check_error_format(self, format: Optional[str] = None):
|
def _check_error_format(self, format: Optional[str] = None):
|
||||||
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
|
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
|
||||||
|
|
||||||
@@ -206,7 +246,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
"""
|
"""
|
||||||
Looks for prefixed environment variables and applies them to the
|
Looks for prefixed environment variables and applies them to the
|
||||||
configuration if present. This is called automatically when Sanic
|
configuration if present. This is called automatically when Sanic
|
||||||
starts up to load environment variables into config.
|
starts up to load environment variables into config. Environment
|
||||||
|
variables should start with the defined prefix and should only
|
||||||
|
contain uppercase letters.
|
||||||
|
|
||||||
It will automatically hydrate the following types:
|
It will automatically hydrate the following types:
|
||||||
|
|
||||||
@@ -232,12 +274,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
`See user guide re: config
|
`See user guide re: config
|
||||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||||
"""
|
"""
|
||||||
lower_case_var_found = False
|
|
||||||
for key, value in environ.items():
|
for key, value in environ.items():
|
||||||
if not key.startswith(prefix):
|
if not key.startswith(prefix) or not key.isupper():
|
||||||
continue
|
continue
|
||||||
if not key.isupper():
|
|
||||||
lower_case_var_found = True
|
|
||||||
|
|
||||||
_, config_key = key.split(prefix, 1)
|
_, config_key = key.split(prefix, 1)
|
||||||
|
|
||||||
@@ -247,12 +286,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if lower_case_var_found:
|
|
||||||
deprecation(
|
|
||||||
"Lowercase environment variables will not be "
|
|
||||||
"loaded into Sanic config beginning in v22.9.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
from enum import Enum, auto
|
from enum import auto
|
||||||
|
|
||||||
|
from sanic.compat import UpperStrEnum
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethod(str, Enum):
|
class HTTPMethod(UpperStrEnum):
|
||||||
def _generate_next_value_(name, start, count, last_values):
|
|
||||||
return name.upper()
|
|
||||||
|
|
||||||
def __eq__(self, value: object) -> bool:
|
|
||||||
value = str(value).upper()
|
|
||||||
return super().__eq__(value)
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self.value)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
GET = auto()
|
GET = auto()
|
||||||
POST = auto()
|
POST = auto()
|
||||||
PUT = auto()
|
PUT = auto()
|
||||||
@@ -24,16 +13,22 @@ class HTTPMethod(str, Enum):
|
|||||||
DELETE = auto()
|
DELETE = auto()
|
||||||
|
|
||||||
|
|
||||||
class LocalCertCreator(str, Enum):
|
class LocalCertCreator(UpperStrEnum):
|
||||||
def _generate_next_value_(name, start, count, last_values):
|
|
||||||
return name.upper()
|
|
||||||
|
|
||||||
AUTO = auto()
|
AUTO = auto()
|
||||||
TRUSTME = auto()
|
TRUSTME = auto()
|
||||||
MKCERT = auto()
|
MKCERT = auto()
|
||||||
|
|
||||||
|
|
||||||
HTTP_METHODS = tuple(HTTPMethod.__members__.values())
|
HTTP_METHODS = tuple(HTTPMethod.__members__.values())
|
||||||
|
SAFE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.OPTIONS)
|
||||||
|
IDEMPOTENT_HTTP_METHODS = (
|
||||||
|
HTTPMethod.GET,
|
||||||
|
HTTPMethod.HEAD,
|
||||||
|
HTTPMethod.PUT,
|
||||||
|
HTTPMethod.DELETE,
|
||||||
|
HTTPMethod.OPTIONS,
|
||||||
|
)
|
||||||
|
CACHEABLE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD)
|
||||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||||
DEFAULT_LOCAL_TLS_KEY = "key.pem"
|
DEFAULT_LOCAL_TLS_KEY = "key.pem"
|
||||||
DEFAULT_LOCAL_TLS_CERT = "cert.pem"
|
DEFAULT_LOCAL_TLS_CERT = "cert.pem"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Setting ``app.config.FALLBACK_ERROR_FORMAT = "auto"`` will enable a switch that
|
|||||||
will attempt to provide an appropriate response format based upon the
|
will attempt to provide an appropriate response format based upon the
|
||||||
request type.
|
request type.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
@@ -21,8 +22,8 @@ from traceback import extract_tb
|
|||||||
|
|
||||||
from sanic.exceptions import BadRequest, SanicException
|
from sanic.exceptions import BadRequest, SanicException
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
from sanic.request import Request
|
from sanic.pages.error import ErrorPage
|
||||||
from sanic.response import HTTPResponse, html, json, text
|
from sanic.response import html, json, text
|
||||||
|
|
||||||
|
|
||||||
dumps: t.Callable[..., str]
|
dumps: t.Callable[..., str]
|
||||||
@@ -33,6 +34,8 @@ try:
|
|||||||
except ImportError: # noqa
|
except ImportError: # noqa
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from sanic import HTTPResponse, Request
|
||||||
|
|
||||||
DEFAULT_FORMAT = "auto"
|
DEFAULT_FORMAT = "auto"
|
||||||
FALLBACK_TEXT = (
|
FALLBACK_TEXT = (
|
||||||
@@ -157,36 +160,21 @@ class HTMLRenderer(BaseRenderer):
|
|||||||
"{body}"
|
"{body}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def full(self) -> HTTPResponse:
|
def _page(self, full: bool) -> HTTPResponse:
|
||||||
return html(
|
page = ErrorPage(
|
||||||
self.OUTPUT_HTML.format(
|
title=super().title,
|
||||||
title=self.title,
|
text=super().text,
|
||||||
text=self.text,
|
request=self.request,
|
||||||
style=self.TRACEBACK_STYLE,
|
exc=self.exception,
|
||||||
body=self._generate_body(full=True),
|
full=full,
|
||||||
),
|
|
||||||
status=self.status,
|
|
||||||
)
|
)
|
||||||
|
return html(page.render(), status=self.status, headers=self.headers)
|
||||||
|
|
||||||
|
def full(self) -> HTTPResponse:
|
||||||
|
return self._page(full=True)
|
||||||
|
|
||||||
def minimal(self) -> HTTPResponse:
|
def minimal(self) -> HTTPResponse:
|
||||||
return html(
|
return self._page(full=False)
|
||||||
self.OUTPUT_HTML.format(
|
|
||||||
title=self.title,
|
|
||||||
text=self.text,
|
|
||||||
style=self.TRACEBACK_STYLE,
|
|
||||||
body=self._generate_body(full=False),
|
|
||||||
),
|
|
||||||
status=self.status,
|
|
||||||
headers=self.headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self):
|
|
||||||
return escape(super().text)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def title(self):
|
|
||||||
return escape(f"⚠️ {super().title}")
|
|
||||||
|
|
||||||
def _generate_body(self, *, full):
|
def _generate_body(self, *, full):
|
||||||
lines = []
|
lines = []
|
||||||
@@ -404,16 +392,13 @@ CONTENT_TYPE_BY_RENDERERS = {
|
|||||||
v: k for k, v in RENDERERS_BY_CONTENT_TYPE.items()
|
v: k for k, v in RENDERERS_BY_CONTENT_TYPE.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Handler source code is checked for which response types it returns with the
|
||||||
|
# route error_format="auto" (default) to determine which format to use.
|
||||||
RESPONSE_MAPPING = {
|
RESPONSE_MAPPING = {
|
||||||
"empty": "html",
|
|
||||||
"json": "json",
|
"json": "json",
|
||||||
"text": "text",
|
"text": "text",
|
||||||
"raw": "text",
|
|
||||||
"html": "html",
|
"html": "html",
|
||||||
"file": "html",
|
"JSONResponse": "json",
|
||||||
"file_stream": "text",
|
|
||||||
"stream": "text",
|
|
||||||
"redirect": "html",
|
|
||||||
"text/plain": "text",
|
"text/plain": "text",
|
||||||
"text/html": "html",
|
"text/html": "html",
|
||||||
"application/json": "json",
|
"application/json": "json",
|
||||||
@@ -448,8 +433,8 @@ def exception_response(
|
|||||||
# from the route
|
# from the route
|
||||||
if request.route:
|
if request.route:
|
||||||
try:
|
try:
|
||||||
if request.route.ctx.error_format:
|
if request.route.extra.error_format:
|
||||||
render_format = request.route.ctx.error_format
|
render_format = request.route.extra.error_format
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
from asyncio import CancelledError
|
||||||
from typing import Any, Dict, Optional, Union
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
|
|
||||||
|
class RequestCancelled(CancelledError):
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
|
class ServerKilled(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class SanicException(Exception):
|
class SanicException(Exception):
|
||||||
message: str = ""
|
message: str = ""
|
||||||
|
|
||||||
|
|||||||
10
sanic/handlers/__init__.py
Normal file
10
sanic/handlers/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from .content_range import ContentRangeHandler
|
||||||
|
from .directory import DirectoryHandler
|
||||||
|
from .error import ErrorHandler
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"ContentRangeHandler",
|
||||||
|
"DirectoryHandler",
|
||||||
|
"ErrorHandler",
|
||||||
|
)
|
||||||
78
sanic/handlers/content_range.py
Normal file
78
sanic/handlers/content_range.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from sanic.exceptions import (
|
||||||
|
HeaderNotFound,
|
||||||
|
InvalidRangeType,
|
||||||
|
RangeNotSatisfiable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentRangeHandler:
|
||||||
|
"""
|
||||||
|
A mechanism to parse and process the incoming request headers to
|
||||||
|
extract the content range information.
|
||||||
|
|
||||||
|
:param request: Incoming api request
|
||||||
|
:param stats: Stats related to the content
|
||||||
|
|
||||||
|
:type request: :class:`sanic.request.Request`
|
||||||
|
:type stats: :class:`posix.stat_result`
|
||||||
|
|
||||||
|
:ivar start: Content Range start
|
||||||
|
:ivar end: Content Range end
|
||||||
|
:ivar size: Length of the content
|
||||||
|
:ivar total: Total size identified by the :class:`posix.stat_result`
|
||||||
|
instance
|
||||||
|
:ivar ContentRangeHandler.headers: Content range header ``dict``
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("start", "end", "size", "total", "headers")
|
||||||
|
|
||||||
|
def __init__(self, request, stats):
|
||||||
|
self.total = stats.st_size
|
||||||
|
_range = request.headers.getone("range", None)
|
||||||
|
if _range is None:
|
||||||
|
raise HeaderNotFound("Range Header Not Found")
|
||||||
|
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||||
|
if unit != "bytes":
|
||||||
|
raise InvalidRangeType(
|
||||||
|
"%s is not a valid Range Type" % (unit,), self
|
||||||
|
)
|
||||||
|
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
|
||||||
|
try:
|
||||||
|
self.start = int(start_b) if start_b else None
|
||||||
|
except ValueError:
|
||||||
|
raise RangeNotSatisfiable(
|
||||||
|
"'%s' is invalid for Content Range" % (start_b,), self
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.end = int(end_b) if end_b else None
|
||||||
|
except ValueError:
|
||||||
|
raise RangeNotSatisfiable(
|
||||||
|
"'%s' is invalid for Content Range" % (end_b,), self
|
||||||
|
)
|
||||||
|
if self.end is None:
|
||||||
|
if self.start is None:
|
||||||
|
raise RangeNotSatisfiable(
|
||||||
|
"Invalid for Content Range parameters", self
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# this case represents `Content-Range: bytes 5-`
|
||||||
|
self.end = self.total - 1
|
||||||
|
else:
|
||||||
|
if self.start is None:
|
||||||
|
# this case represents `Content-Range: bytes -5`
|
||||||
|
self.start = self.total - self.end
|
||||||
|
self.end = self.total - 1
|
||||||
|
if self.start >= self.end:
|
||||||
|
raise RangeNotSatisfiable(
|
||||||
|
"Invalid for Content Range parameters", self
|
||||||
|
)
|
||||||
|
self.size = self.end - self.start + 1
|
||||||
|
self.headers = {
|
||||||
|
"Content-Range": "bytes %s-%s/%s"
|
||||||
|
% (self.start, self.end, self.total)
|
||||||
|
}
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self.size > 0
|
||||||
84
sanic/handlers/directory.py
Normal file
84
sanic/handlers/directory.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from operator import itemgetter
|
||||||
|
from pathlib import Path
|
||||||
|
from stat import S_ISDIR
|
||||||
|
from typing import Dict, Iterable, Optional, Sequence, Union, cast
|
||||||
|
|
||||||
|
from sanic.exceptions import NotFound
|
||||||
|
from sanic.pages.directory_page import DirectoryPage, FileInfo
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import file, html, redirect
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryHandler:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
uri: str,
|
||||||
|
directory: Path,
|
||||||
|
directory_view: bool = False,
|
||||||
|
index: Optional[Union[str, Sequence[str]]] = None,
|
||||||
|
) -> None:
|
||||||
|
if isinstance(index, str):
|
||||||
|
index = [index]
|
||||||
|
elif index is None:
|
||||||
|
index = []
|
||||||
|
self.base = uri.strip("/")
|
||||||
|
self.directory = directory
|
||||||
|
self.directory_view = directory_view
|
||||||
|
self.index = tuple(index)
|
||||||
|
|
||||||
|
async def handle(self, request: Request, path: str):
|
||||||
|
current = path.strip("/")[len(self.base) :].strip("/") # noqa: E203
|
||||||
|
for file_name in self.index:
|
||||||
|
index_file = self.directory / current / file_name
|
||||||
|
if index_file.is_file():
|
||||||
|
return await file(index_file)
|
||||||
|
|
||||||
|
if self.directory_view:
|
||||||
|
return self._index(
|
||||||
|
self.directory / current, path, request.app.debug
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.index:
|
||||||
|
raise NotFound("File not found")
|
||||||
|
|
||||||
|
raise IsADirectoryError(f"{self.directory.as_posix()} is a directory")
|
||||||
|
|
||||||
|
def _index(self, location: Path, path: str, debug: bool):
|
||||||
|
# Remove empty path elements, append slash
|
||||||
|
if "//" in path or not path.endswith("/"):
|
||||||
|
return redirect(
|
||||||
|
"/" + "".join([f"{p}/" for p in path.split("/") if p])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Render file browser
|
||||||
|
page = DirectoryPage(self._iter_files(location), path, debug)
|
||||||
|
return html(page.render())
|
||||||
|
|
||||||
|
def _prepare_file(self, path: Path) -> Dict[str, Union[int, str]]:
|
||||||
|
stat = path.stat()
|
||||||
|
modified = (
|
||||||
|
datetime.fromtimestamp(stat.st_mtime)
|
||||||
|
.isoformat()[:19]
|
||||||
|
.replace("T", " ")
|
||||||
|
)
|
||||||
|
is_dir = S_ISDIR(stat.st_mode)
|
||||||
|
icon = "📁" if is_dir else "📄"
|
||||||
|
file_name = path.name
|
||||||
|
if is_dir:
|
||||||
|
file_name += "/"
|
||||||
|
return {
|
||||||
|
"priority": is_dir * -1,
|
||||||
|
"file_name": file_name,
|
||||||
|
"icon": icon,
|
||||||
|
"file_access": modified,
|
||||||
|
"file_size": stat.st_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _iter_files(self, location: Path) -> Iterable[FileInfo]:
|
||||||
|
prepared = [self._prepare_file(f) for f in location.iterdir()]
|
||||||
|
for item in sorted(prepared, key=itemgetter("priority", "file_name")):
|
||||||
|
del item["priority"]
|
||||||
|
yield cast(FileInfo, item)
|
||||||
@@ -3,11 +3,6 @@ from __future__ import annotations
|
|||||||
from typing import Dict, List, Optional, Tuple, Type
|
from typing import Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
|
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
|
||||||
from sanic.exceptions import (
|
|
||||||
HeaderNotFound,
|
|
||||||
InvalidRangeType,
|
|
||||||
RangeNotSatisfiable,
|
|
||||||
)
|
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import deprecation, error_logger
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
@@ -23,7 +18,6 @@ class ErrorHandler:
|
|||||||
by the developers to perform a wide range of tasks from recording the error
|
by the developers to perform a wide range of tasks from recording the error
|
||||||
stats to reporting them to an external service that can be used for
|
stats to reporting them to an external service that can be used for
|
||||||
realtime alerting system.
|
realtime alerting system.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -36,17 +30,31 @@ class ErrorHandler:
|
|||||||
self.debug = False
|
self.debug = False
|
||||||
self.base = base
|
self.base = base
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def finalize(cls, *args, **kwargs):
|
|
||||||
deprecation(
|
|
||||||
"ErrorHandler.finalize is deprecated and no longer needed. "
|
|
||||||
"Please remove update your code to remove it. ",
|
|
||||||
22.12,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
def _full_lookup(self, exception, route_name: Optional[str] = None):
|
||||||
return self.lookup(exception, route_name)
|
return self.lookup(exception, route_name)
|
||||||
|
|
||||||
|
def _add(
|
||||||
|
self,
|
||||||
|
key: Tuple[Type[BaseException], Optional[str]],
|
||||||
|
handler: RouteHandler,
|
||||||
|
) -> None:
|
||||||
|
if key in self.cached_handlers:
|
||||||
|
exc, name = key
|
||||||
|
if name is None:
|
||||||
|
name = "__ALL_ROUTES__"
|
||||||
|
|
||||||
|
error_logger.warning(
|
||||||
|
f"Duplicate exception handler definition on: route={name} "
|
||||||
|
f"and exception={exc}"
|
||||||
|
)
|
||||||
|
deprecation(
|
||||||
|
"A duplicate exception handler definition was discovered. "
|
||||||
|
"This may cause unintended consequences. A warning has been "
|
||||||
|
"issued now, but it will not be allowed starting in v23.3.",
|
||||||
|
23.3,
|
||||||
|
)
|
||||||
|
self.cached_handlers[key] = handler
|
||||||
|
|
||||||
def add(self, exception, handler, route_names: Optional[List[str]] = None):
|
def add(self, exception, handler, route_names: Optional[List[str]] = None):
|
||||||
"""
|
"""
|
||||||
Add a new exception handler to an already existing handler object.
|
Add a new exception handler to an already existing handler object.
|
||||||
@@ -62,9 +70,9 @@ class ErrorHandler:
|
|||||||
"""
|
"""
|
||||||
if route_names:
|
if route_names:
|
||||||
for route in route_names:
|
for route in route_names:
|
||||||
self.cached_handlers[(exception, route)] = handler
|
self._add((exception, route), handler)
|
||||||
else:
|
else:
|
||||||
self.cached_handlers[(exception, None)] = handler
|
self._add((exception, None), handler)
|
||||||
|
|
||||||
def lookup(self, exception, route_name: Optional[str] = None):
|
def lookup(self, exception, route_name: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
@@ -182,74 +190,3 @@ class ErrorHandler:
|
|||||||
error_logger.exception(
|
error_logger.exception(
|
||||||
"Exception occurred while handling uri: %s", url
|
"Exception occurred while handling uri: %s", url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentRangeHandler:
|
|
||||||
"""
|
|
||||||
A mechanism to parse and process the incoming request headers to
|
|
||||||
extract the content range information.
|
|
||||||
|
|
||||||
:param request: Incoming api request
|
|
||||||
:param stats: Stats related to the content
|
|
||||||
|
|
||||||
:type request: :class:`sanic.request.Request`
|
|
||||||
:type stats: :class:`posix.stat_result`
|
|
||||||
|
|
||||||
:ivar start: Content Range start
|
|
||||||
:ivar end: Content Range end
|
|
||||||
:ivar size: Length of the content
|
|
||||||
:ivar total: Total size identified by the :class:`posix.stat_result`
|
|
||||||
instance
|
|
||||||
:ivar ContentRangeHandler.headers: Content range header ``dict``
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("start", "end", "size", "total", "headers")
|
|
||||||
|
|
||||||
def __init__(self, request, stats):
|
|
||||||
self.total = stats.st_size
|
|
||||||
_range = request.headers.getone("range", None)
|
|
||||||
if _range is None:
|
|
||||||
raise HeaderNotFound("Range Header Not Found")
|
|
||||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
|
||||||
if unit != "bytes":
|
|
||||||
raise InvalidRangeType(
|
|
||||||
"%s is not a valid Range Type" % (unit,), self
|
|
||||||
)
|
|
||||||
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
|
|
||||||
try:
|
|
||||||
self.start = int(start_b) if start_b else None
|
|
||||||
except ValueError:
|
|
||||||
raise RangeNotSatisfiable(
|
|
||||||
"'%s' is invalid for Content Range" % (start_b,), self
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
self.end = int(end_b) if end_b else None
|
|
||||||
except ValueError:
|
|
||||||
raise RangeNotSatisfiable(
|
|
||||||
"'%s' is invalid for Content Range" % (end_b,), self
|
|
||||||
)
|
|
||||||
if self.end is None:
|
|
||||||
if self.start is None:
|
|
||||||
raise RangeNotSatisfiable(
|
|
||||||
"Invalid for Content Range parameters", self
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# this case represents `Content-Range: bytes 5-`
|
|
||||||
self.end = self.total - 1
|
|
||||||
else:
|
|
||||||
if self.start is None:
|
|
||||||
# this case represents `Content-Range: bytes -5`
|
|
||||||
self.start = self.total - self.end
|
|
||||||
self.end = self.total - 1
|
|
||||||
if self.start >= self.end:
|
|
||||||
raise RangeNotSatisfiable(
|
|
||||||
"Invalid for Content Range parameters", self
|
|
||||||
)
|
|
||||||
self.size = self.end - self.start + 1
|
|
||||||
self.headers = {
|
|
||||||
"Content-Range": "bytes %s-%s/%s"
|
|
||||||
% (self.start, self.end, self.total)
|
|
||||||
}
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return self.size > 0
|
|
||||||
@@ -14,6 +14,7 @@ from sanic.exceptions import (
|
|||||||
BadRequest,
|
BadRequest,
|
||||||
ExpectationFailed,
|
ExpectationFailed,
|
||||||
PayloadTooLarge,
|
PayloadTooLarge,
|
||||||
|
RequestCancelled,
|
||||||
ServerError,
|
ServerError,
|
||||||
ServiceUnavailable,
|
ServiceUnavailable,
|
||||||
)
|
)
|
||||||
@@ -70,7 +71,6 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
"request_body",
|
"request_body",
|
||||||
"request_bytes",
|
"request_bytes",
|
||||||
"request_bytes_left",
|
"request_bytes_left",
|
||||||
"request_max_size",
|
|
||||||
"response",
|
"response",
|
||||||
"response_func",
|
"response_func",
|
||||||
"response_size",
|
"response_size",
|
||||||
@@ -132,7 +132,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
if self.stage is Stage.RESPONSE:
|
if self.stage is Stage.RESPONSE:
|
||||||
await self.response.send(end_stream=True)
|
await self.response.send(end_stream=True)
|
||||||
except CancelledError:
|
except CancelledError as exc:
|
||||||
# Write an appropriate response before exiting
|
# Write an appropriate response before exiting
|
||||||
if not self.protocol.transport:
|
if not self.protocol.transport:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -140,7 +140,11 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
"stopped. Transport is closed."
|
"stopped. Transport is closed."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
e = self.exception or ServiceUnavailable("Cancelled")
|
e = (
|
||||||
|
RequestCancelled()
|
||||||
|
if self.protocol.conn_info.lost
|
||||||
|
else (self.exception or exc)
|
||||||
|
)
|
||||||
self.exception = None
|
self.exception = None
|
||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
await self.error_response(e)
|
await self.error_response(e)
|
||||||
@@ -424,7 +428,13 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
if self.request is None:
|
if self.request is None:
|
||||||
self.create_empty_request()
|
self.create_empty_request()
|
||||||
|
|
||||||
await app.handle_exception(self.request, exception)
|
request_middleware = not isinstance(exception, ServiceUnavailable)
|
||||||
|
try:
|
||||||
|
await app.handle_exception(
|
||||||
|
self.request, exception, request_middleware
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await app.handle_exception(self.request, e, False)
|
||||||
|
|
||||||
def create_empty_request(self) -> None:
|
def create_empty_request(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from sanic.exceptions import PayloadTooLarge, SanicException, ServerError
|
|||||||
from sanic.helpers import has_message_body
|
from sanic.helpers import has_message_body
|
||||||
from sanic.http.constants import Stage
|
from sanic.http.constants import Stage
|
||||||
from sanic.http.stream import Stream
|
from sanic.http.stream import Stream
|
||||||
from sanic.http.tls.context import CertSelector, CertSimple, SanicSSLContext
|
from sanic.http.tls.context import CertSelector, SanicSSLContext
|
||||||
from sanic.log import Colors, logger
|
from sanic.log import Colors, logger
|
||||||
from sanic.models.protocol_types import TransportProtocol
|
from sanic.models.protocol_types import TransportProtocol
|
||||||
from sanic.models.server_types import ConnInfo
|
from sanic.models.server_types import ConnInfo
|
||||||
@@ -378,7 +378,7 @@ def get_config(
|
|||||||
app: Sanic, ssl: Union[SanicSSLContext, CertSelector, SSLContext]
|
app: Sanic, ssl: Union[SanicSSLContext, CertSelector, SSLContext]
|
||||||
):
|
):
|
||||||
# TODO:
|
# TODO:
|
||||||
# - proper selection needed if servince with multiple certs insted of
|
# - proper selection needed if service with multiple certs insted of
|
||||||
# just taking the first
|
# just taking the first
|
||||||
if isinstance(ssl, CertSelector):
|
if isinstance(ssl, CertSelector):
|
||||||
ssl = cast(SanicSSLContext, ssl.sanic_select[0])
|
ssl = cast(SanicSSLContext, ssl.sanic_select[0])
|
||||||
@@ -389,8 +389,8 @@ def get_config(
|
|||||||
"should be able to use mkcert instead. For more information, see: "
|
"should be able to use mkcert instead. For more information, see: "
|
||||||
"https://github.com/aiortc/aioquic/issues/295."
|
"https://github.com/aiortc/aioquic/issues/295."
|
||||||
)
|
)
|
||||||
if not isinstance(ssl, CertSimple):
|
if not isinstance(ssl, SanicSSLContext):
|
||||||
raise SanicException("SSLContext is not CertSimple")
|
raise SanicException("SSLContext is not SanicSSLContext")
|
||||||
|
|
||||||
config = QuicConfiguration(
|
config = QuicConfiguration(
|
||||||
alpn_protocols=H3_ALPN + H0_ALPN + ["siduck"],
|
alpn_protocols=H3_ALPN + H0_ALPN + ["siduck"],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Stream:
|
|||||||
request_max_size: Union[int, float]
|
request_max_size: Union[int, float]
|
||||||
|
|
||||||
__touchup__: Tuple[str, ...] = tuple()
|
__touchup__: Tuple[str, ...] = tuple()
|
||||||
__slots__ = ()
|
__slots__ = ("request_max_size",)
|
||||||
|
|
||||||
def respond(
|
def respond(
|
||||||
self, response: BaseHTTPResponse
|
self, response: BaseHTTPResponse
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ def create_context(
|
|||||||
certfile: Optional[str] = None,
|
certfile: Optional[str] = None,
|
||||||
keyfile: Optional[str] = None,
|
keyfile: Optional[str] = None,
|
||||||
password: Optional[str] = None,
|
password: Optional[str] = None,
|
||||||
|
purpose: ssl.Purpose = ssl.Purpose.CLIENT_AUTH,
|
||||||
) -> ssl.SSLContext:
|
) -> ssl.SSLContext:
|
||||||
"""Create a context with secure crypto and HTTP/1.1 in protocols."""
|
"""Create a context with secure crypto and HTTP/1.1 in protocols."""
|
||||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
context = ssl.create_default_context(purpose=purpose)
|
||||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||||
context.set_ciphers(":".join(CIPHERS_TLS12))
|
context.set_ciphers(":".join(CIPHERS_TLS12))
|
||||||
context.set_alpn_protocols(["http/1.1"])
|
context.set_alpn_protocols(["http/1.1"])
|
||||||
context.sni_callback = server_name_callback
|
if purpose is ssl.Purpose.CLIENT_AUTH:
|
||||||
|
context.sni_callback = server_name_callback
|
||||||
if certfile and keyfile:
|
if certfile and keyfile:
|
||||||
context.load_cert_chain(certfile, keyfile, password)
|
context.load_cert_chain(certfile, keyfile, password)
|
||||||
return context
|
return context
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ def get_ssl_context(
|
|||||||
"without passing a TLS certificate. If you are developing "
|
"without passing a TLS certificate. If you are developing "
|
||||||
"locally, please enable DEVELOPMENT mode and Sanic will "
|
"locally, please enable DEVELOPMENT mode and Sanic will "
|
||||||
"generate a localhost TLS certificate. For more information "
|
"generate a localhost TLS certificate. For more information "
|
||||||
"please see: ___."
|
"please see: https://sanic.dev/en/guide/deployment/development."
|
||||||
|
"html#automatic-tls-certificate."
|
||||||
)
|
)
|
||||||
|
|
||||||
creator = CertCreator.select(
|
creator = CertCreator.select(
|
||||||
@@ -125,7 +126,6 @@ class CertCreator(ABC):
|
|||||||
local_tls_key,
|
local_tls_key,
|
||||||
local_tls_cert,
|
local_tls_cert,
|
||||||
) -> CertCreator:
|
) -> CertCreator:
|
||||||
|
|
||||||
creator: Optional[CertCreator] = None
|
creator: Optional[CertCreator] = None
|
||||||
|
|
||||||
cert_creator_options: Tuple[
|
cert_creator_options: Tuple[
|
||||||
@@ -151,7 +151,8 @@ class CertCreator(ABC):
|
|||||||
raise SanicException(
|
raise SanicException(
|
||||||
"Sanic could not find package to create a TLS certificate. "
|
"Sanic could not find package to create a TLS certificate. "
|
||||||
"You must have either mkcert or trustme installed. See "
|
"You must have either mkcert or trustme installed. See "
|
||||||
"_____ for more details."
|
"https://sanic.dev/en/guide/deployment/development.html"
|
||||||
|
"#automatic-tls-certificate for more details."
|
||||||
)
|
)
|
||||||
|
|
||||||
return creator
|
return creator
|
||||||
@@ -203,7 +204,8 @@ class MkcertCreator(CertCreator):
|
|||||||
"to proceed. Installation instructions can be found here: "
|
"to proceed. Installation instructions can be found here: "
|
||||||
"https://github.com/FiloSottile/mkcert.\n"
|
"https://github.com/FiloSottile/mkcert.\n"
|
||||||
"Find out more information about your options here: "
|
"Find out more information about your options here: "
|
||||||
"_____"
|
"https://sanic.dev/en/guide/deployment/development.html#"
|
||||||
|
"automatic-tls-certificate"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
def generate_cert(self, localhost: str) -> ssl.SSLContext:
|
def generate_cert(self, localhost: str) -> ssl.SSLContext:
|
||||||
@@ -240,7 +242,12 @@ class MkcertCreator(CertCreator):
|
|||||||
self.cert_path.unlink()
|
self.cert_path.unlink()
|
||||||
self.tmpdir.rmdir()
|
self.tmpdir.rmdir()
|
||||||
|
|
||||||
return CertSimple(self.cert_path, self.key_path)
|
context = CertSimple(self.cert_path, self.key_path)
|
||||||
|
context.sanic["creator"] = "mkcert"
|
||||||
|
context.sanic["localhost"] = localhost
|
||||||
|
SanicSSLContext.create_from_ssl_context(context)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class TrustmeCreator(CertCreator):
|
class TrustmeCreator(CertCreator):
|
||||||
@@ -255,24 +262,28 @@ class TrustmeCreator(CertCreator):
|
|||||||
"to proceed. Installation instructions can be found here: "
|
"to proceed. Installation instructions can be found here: "
|
||||||
"https://github.com/python-trio/trustme.\n"
|
"https://github.com/python-trio/trustme.\n"
|
||||||
"Find out more information about your options here: "
|
"Find out more information about your options here: "
|
||||||
"_____"
|
"https://sanic.dev/en/guide/deployment/development.html#"
|
||||||
|
"automatic-tls-certificate"
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_cert(self, localhost: str) -> ssl.SSLContext:
|
def generate_cert(self, localhost: str) -> ssl.SSLContext:
|
||||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
context = SanicSSLContext.create_from_ssl_context(
|
||||||
sanic_context = SanicSSLContext.create_from_ssl_context(context)
|
ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
sanic_context.sanic = {
|
)
|
||||||
|
context.sanic = {
|
||||||
"cert": self.cert_path.absolute(),
|
"cert": self.cert_path.absolute(),
|
||||||
"key": self.key_path.absolute(),
|
"key": self.key_path.absolute(),
|
||||||
}
|
}
|
||||||
ca = trustme.CA()
|
ca = trustme.CA()
|
||||||
server_cert = ca.issue_cert(localhost)
|
server_cert = ca.issue_cert(localhost)
|
||||||
server_cert.configure_cert(sanic_context)
|
server_cert.configure_cert(context)
|
||||||
ca.configure_trust(context)
|
ca.configure_trust(context)
|
||||||
|
|
||||||
ca.cert_pem.write_to_path(str(self.cert_path.absolute()))
|
ca.cert_pem.write_to_path(str(self.cert_path.absolute()))
|
||||||
server_cert.private_key_and_cert_chain_pem.write_to_path(
|
server_cert.private_key_and_cert_chain_pem.write_to_path(
|
||||||
str(self.key_path.absolute())
|
str(self.key_path.absolute())
|
||||||
)
|
)
|
||||||
|
context.sanic["creator"] = "trustme"
|
||||||
|
context.sanic["localhost"] = localhost
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|||||||
36
sanic/log.py
36
sanic/log.py
@@ -2,12 +2,23 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict
|
from typing import TYPE_CHECKING, Any, Dict
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.compat import is_atty
|
from sanic.compat import is_atty
|
||||||
|
|
||||||
|
|
||||||
|
# Python 3.11 changed the way Enum formatting works for mixed-in types.
|
||||||
|
if sys.version_info < (3, 11, 0):
|
||||||
|
|
||||||
|
class StrEnum(str, Enum):
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not TYPE_CHECKING:
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||||
version=1,
|
version=1,
|
||||||
disable_existing_loggers=False,
|
disable_existing_loggers=False,
|
||||||
@@ -25,6 +36,12 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
|||||||
"propagate": True,
|
"propagate": True,
|
||||||
"qualname": "sanic.access",
|
"qualname": "sanic.access",
|
||||||
},
|
},
|
||||||
|
"sanic.server": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["console"],
|
||||||
|
"propagate": True,
|
||||||
|
"qualname": "sanic.server",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handlers={
|
handlers={
|
||||||
"console": {
|
"console": {
|
||||||
@@ -62,12 +79,13 @@ Defult logging configuration
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Colors(str, Enum): # no cov
|
class Colors(StrEnum): # no cov
|
||||||
END = "\033[0m"
|
END = "\033[0m"
|
||||||
BLUE = "\033[01;34m"
|
BOLD = "\033[1m"
|
||||||
GREEN = "\033[01;32m"
|
BLUE = "\033[34m"
|
||||||
PURPLE = "\033[01;35m"
|
GREEN = "\033[32m"
|
||||||
RED = "\033[01;31m"
|
PURPLE = "\033[35m"
|
||||||
|
RED = "\033[31m"
|
||||||
SANIC = "\033[38;2;255;13;104m"
|
SANIC = "\033[38;2;255;13;104m"
|
||||||
YELLOW = "\033[01;33m"
|
YELLOW = "\033[01;33m"
|
||||||
|
|
||||||
@@ -100,6 +118,12 @@ Logger used by Sanic for access logging
|
|||||||
"""
|
"""
|
||||||
access_logger.addFilter(_verbosity_filter)
|
access_logger.addFilter(_verbosity_filter)
|
||||||
|
|
||||||
|
server_logger = logging.getLogger("sanic.server") # no cov
|
||||||
|
"""
|
||||||
|
Logger used by Sanic for server related messages
|
||||||
|
"""
|
||||||
|
logger.addFilter(_verbosity_filter)
|
||||||
|
|
||||||
|
|
||||||
def deprecation(message: str, version: float): # no cov
|
def deprecation(message: str, version: float): # no cov
|
||||||
version_info = f"[DEPRECATION v{version}] "
|
version_info = f"[DEPRECATION v{version}] "
|
||||||
|
|||||||
69
sanic/middleware.py
Normal file
69
sanic/middleware.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from enum import IntEnum, auto
|
||||||
|
from itertools import count
|
||||||
|
from typing import Deque, Sequence, Union
|
||||||
|
|
||||||
|
from sanic.models.handler_types import MiddlewareType
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareLocation(IntEnum):
|
||||||
|
REQUEST = auto()
|
||||||
|
RESPONSE = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Middleware:
|
||||||
|
_counter = count()
|
||||||
|
|
||||||
|
__slots__ = ("func", "priority", "location", "definition")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
func: MiddlewareType,
|
||||||
|
location: MiddlewareLocation,
|
||||||
|
priority: int = 0,
|
||||||
|
) -> None:
|
||||||
|
self.func = func
|
||||||
|
self.priority = priority
|
||||||
|
self.location = location
|
||||||
|
self.definition = next(Middleware._counter)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self.func(*args, **kwargs)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.func)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.__class__.__name__}("
|
||||||
|
f"func=<function {self.func.__name__}>, "
|
||||||
|
f"priority={self.priority}, "
|
||||||
|
f"location={self.location.name})"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def order(self):
|
||||||
|
return (self.priority, -self.definition)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert(
|
||||||
|
cls,
|
||||||
|
*middleware_collections: Sequence[Union[Middleware, MiddlewareType]],
|
||||||
|
location: MiddlewareLocation,
|
||||||
|
) -> Deque[Middleware]:
|
||||||
|
return deque(
|
||||||
|
[
|
||||||
|
middleware
|
||||||
|
if isinstance(middleware, Middleware)
|
||||||
|
else Middleware(middleware, location)
|
||||||
|
for collection in middleware_collections
|
||||||
|
for middleware in collection
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_count(cls):
|
||||||
|
cls._counter = count()
|
||||||
|
cls.count = next(cls._counter)
|
||||||
35
sanic/mixins/base.py
Normal file
35
sanic/mixins/base.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sanic.base.meta import SanicMeta
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMixin(metaclass=SanicMeta):
|
||||||
|
name: str
|
||||||
|
strict_slashes: Optional[bool]
|
||||||
|
|
||||||
|
def _generate_name(self, *objects) -> str:
|
||||||
|
name = None
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
if obj:
|
||||||
|
if isinstance(obj, str):
|
||||||
|
name = obj
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
name = obj.name
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
name = obj.__name__
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not name: # noqa
|
||||||
|
raise ValueError("Could not generate a name for handler")
|
||||||
|
|
||||||
|
if not name.startswith(f"{self.name}."):
|
||||||
|
name = f"{self.name}.{name}"
|
||||||
|
|
||||||
|
return name
|
||||||
@@ -17,9 +17,12 @@ class ListenerEvent(str, Enum):
|
|||||||
BEFORE_SERVER_STOP = "server.shutdown.before"
|
BEFORE_SERVER_STOP = "server.shutdown.before"
|
||||||
AFTER_SERVER_STOP = "server.shutdown.after"
|
AFTER_SERVER_STOP = "server.shutdown.after"
|
||||||
MAIN_PROCESS_START = auto()
|
MAIN_PROCESS_START = auto()
|
||||||
|
MAIN_PROCESS_READY = auto()
|
||||||
MAIN_PROCESS_STOP = auto()
|
MAIN_PROCESS_STOP = auto()
|
||||||
RELOAD_PROCESS_START = auto()
|
RELOAD_PROCESS_START = auto()
|
||||||
RELOAD_PROCESS_STOP = auto()
|
RELOAD_PROCESS_STOP = auto()
|
||||||
|
BEFORE_RELOAD_TRIGGER = auto()
|
||||||
|
AFTER_RELOAD_TRIGGER = auto()
|
||||||
|
|
||||||
|
|
||||||
class ListenerMixin(metaclass=SanicMeta):
|
class ListenerMixin(metaclass=SanicMeta):
|
||||||
@@ -98,6 +101,11 @@ class ListenerMixin(metaclass=SanicMeta):
|
|||||||
) -> ListenerType[Sanic]:
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "main_process_start")
|
return self.listener(listener, "main_process_start")
|
||||||
|
|
||||||
|
def main_process_ready(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
|
return self.listener(listener, "main_process_ready")
|
||||||
|
|
||||||
def main_process_stop(
|
def main_process_stop(
|
||||||
self, listener: ListenerType[Sanic]
|
self, listener: ListenerType[Sanic]
|
||||||
) -> ListenerType[Sanic]:
|
) -> ListenerType[Sanic]:
|
||||||
@@ -113,6 +121,16 @@ class ListenerMixin(metaclass=SanicMeta):
|
|||||||
) -> ListenerType[Sanic]:
|
) -> ListenerType[Sanic]:
|
||||||
return self.listener(listener, "reload_process_stop")
|
return self.listener(listener, "reload_process_stop")
|
||||||
|
|
||||||
|
def before_reload_trigger(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
|
return self.listener(listener, "before_reload_trigger")
|
||||||
|
|
||||||
|
def after_reload_trigger(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
|
return self.listener(listener, "after_reload_trigger")
|
||||||
|
|
||||||
def before_server_start(
|
def before_server_start(
|
||||||
self, listener: ListenerType[Sanic]
|
self, listener: ListenerType[Sanic]
|
||||||
) -> ListenerType[Sanic]:
|
) -> ListenerType[Sanic]:
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
|
from collections import deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from operator import attrgetter
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
|
from sanic.middleware import Middleware, MiddlewareLocation
|
||||||
from sanic.models.futures import FutureMiddleware
|
from sanic.models.futures import FutureMiddleware
|
||||||
|
from sanic.router import Router
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareMixin(metaclass=SanicMeta):
|
class MiddlewareMixin(metaclass=SanicMeta):
|
||||||
|
router: Router
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_middleware: List[FutureMiddleware] = []
|
self._future_middleware: List[FutureMiddleware] = []
|
||||||
|
self.wrappers = []
|
||||||
|
|
||||||
def _apply_middleware(self, middleware: FutureMiddleware):
|
def _apply_middleware(self, middleware: FutureMiddleware):
|
||||||
raise NotImplementedError # noqa
|
raise NotImplementedError # noqa
|
||||||
|
|
||||||
def middleware(
|
def middleware(
|
||||||
self, middleware_or_request, attach_to="request", apply=True
|
self,
|
||||||
|
middleware_or_request,
|
||||||
|
attach_to="request",
|
||||||
|
apply=True,
|
||||||
|
*,
|
||||||
|
priority=0
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate and register middleware to be called before a request
|
Decorate and register middleware to be called before a request
|
||||||
@@ -30,6 +42,12 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
def register_middleware(middleware, attach_to="request"):
|
def register_middleware(middleware, attach_to="request"):
|
||||||
nonlocal apply
|
nonlocal apply
|
||||||
|
|
||||||
|
location = (
|
||||||
|
MiddlewareLocation.REQUEST
|
||||||
|
if attach_to == "request"
|
||||||
|
else MiddlewareLocation.RESPONSE
|
||||||
|
)
|
||||||
|
middleware = Middleware(middleware, location, priority=priority)
|
||||||
future_middleware = FutureMiddleware(middleware, attach_to)
|
future_middleware = FutureMiddleware(middleware, attach_to)
|
||||||
self._future_middleware.append(future_middleware)
|
self._future_middleware.append(future_middleware)
|
||||||
if apply:
|
if apply:
|
||||||
@@ -46,7 +64,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
register_middleware, attach_to=middleware_or_request
|
register_middleware, attach_to=middleware_or_request
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_request(self, middleware=None):
|
def on_request(self, middleware=None, *, priority=0):
|
||||||
"""Register a middleware to be called before a request is handled.
|
"""Register a middleware to be called before a request is handled.
|
||||||
|
|
||||||
This is the same as *@app.middleware('request')*.
|
This is the same as *@app.middleware('request')*.
|
||||||
@@ -54,11 +72,13 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
:param: middleware: A callable that takes in request.
|
:param: middleware: A callable that takes in request.
|
||||||
"""
|
"""
|
||||||
if callable(middleware):
|
if callable(middleware):
|
||||||
return self.middleware(middleware, "request")
|
return self.middleware(middleware, "request", priority=priority)
|
||||||
else:
|
else:
|
||||||
return partial(self.middleware, attach_to="request")
|
return partial(
|
||||||
|
self.middleware, attach_to="request", priority=priority
|
||||||
|
)
|
||||||
|
|
||||||
def on_response(self, middleware=None):
|
def on_response(self, middleware=None, *, priority=0):
|
||||||
"""Register a middleware to be called after a response is created.
|
"""Register a middleware to be called after a response is created.
|
||||||
|
|
||||||
This is the same as *@app.middleware('response')*.
|
This is the same as *@app.middleware('response')*.
|
||||||
@@ -67,6 +87,61 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
A callable that takes in a request and its response.
|
A callable that takes in a request and its response.
|
||||||
"""
|
"""
|
||||||
if callable(middleware):
|
if callable(middleware):
|
||||||
return self.middleware(middleware, "response")
|
return self.middleware(middleware, "response", priority=priority)
|
||||||
else:
|
else:
|
||||||
return partial(self.middleware, attach_to="response")
|
return partial(
|
||||||
|
self.middleware, attach_to="response", priority=priority
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize_middleware(self):
|
||||||
|
for route in self.router.routes:
|
||||||
|
request_middleware = Middleware.convert(
|
||||||
|
self.request_middleware,
|
||||||
|
self.named_request_middleware.get(route.name, deque()),
|
||||||
|
location=MiddlewareLocation.REQUEST,
|
||||||
|
)
|
||||||
|
response_middleware = Middleware.convert(
|
||||||
|
self.response_middleware,
|
||||||
|
self.named_response_middleware.get(route.name, deque()),
|
||||||
|
location=MiddlewareLocation.RESPONSE,
|
||||||
|
)
|
||||||
|
route.extra.request_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
request_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
route.extra.response_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
response_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)[::-1]
|
||||||
|
)
|
||||||
|
request_middleware = Middleware.convert(
|
||||||
|
self.request_middleware,
|
||||||
|
location=MiddlewareLocation.REQUEST,
|
||||||
|
)
|
||||||
|
response_middleware = Middleware.convert(
|
||||||
|
self.response_middleware,
|
||||||
|
location=MiddlewareLocation.RESPONSE,
|
||||||
|
)
|
||||||
|
self.request_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
request_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.response_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
response_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)[::-1]
|
||||||
|
)
|
||||||
|
|
||||||
|
def wrap(self, handler):
|
||||||
|
self.wrappers.append(handler)
|
||||||
|
return handler
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
from ast import NodeVisitor, Return, parse
|
from ast import NodeVisitor, Return, parse
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from functools import partial, wraps
|
|
||||||
from inspect import getsource, signature
|
from inspect import getsource, signature
|
||||||
from mimetypes import guess_type
|
|
||||||
from os import path
|
|
||||||
from pathlib import Path, PurePath
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from time import gmtime, strftime
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -18,50 +14,31 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
from sanic_routing.route import Route
|
from sanic_routing.route import Route
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
from sanic.compat import stat_async
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
|
||||||
from sanic.errorpages import RESPONSE_MAPPING
|
from sanic.errorpages import RESPONSE_MAPPING
|
||||||
from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable
|
from sanic.mixins.base import BaseMixin
|
||||||
from sanic.handlers import ContentRangeHandler
|
|
||||||
from sanic.log import error_logger
|
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
|
||||||
from sanic.types import HashableDict
|
from sanic.types import HashableDict
|
||||||
|
|
||||||
|
|
||||||
RouteWrapper = Callable[
|
RouteWrapper = Callable[
|
||||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||||
]
|
]
|
||||||
RESTRICTED_ROUTE_CONTEXT = (
|
|
||||||
"ignore_body",
|
|
||||||
"stream",
|
|
||||||
"hosts",
|
|
||||||
"static",
|
|
||||||
"error_format",
|
|
||||||
"websocket",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin(metaclass=SanicMeta):
|
class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||||
name: str
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_routes: Set[FutureRoute] = set()
|
self._future_routes: Set[FutureRoute] = set()
|
||||||
self._future_statics: Set[FutureStatic] = set()
|
self._future_statics: Set[FutureStatic] = set()
|
||||||
self.strict_slashes: Optional[bool] = False
|
|
||||||
|
|
||||||
def _apply_route(self, route: FutureRoute) -> List[Route]:
|
def _apply_route(self, route: FutureRoute) -> List[Route]:
|
||||||
raise NotImplementedError # noqa
|
raise NotImplementedError # noqa
|
||||||
|
|
||||||
def _apply_static(self, static: FutureStatic) -> Route:
|
|
||||||
raise NotImplementedError # noqa
|
|
||||||
|
|
||||||
def route(
|
def route(
|
||||||
self,
|
self,
|
||||||
uri: str,
|
uri: str,
|
||||||
@@ -225,7 +202,8 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
unquote: bool = False,
|
||||||
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""A helper method to register class instance or
|
"""A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
@@ -271,6 +249,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name=name,
|
name=name,
|
||||||
version_prefix=version_prefix,
|
version_prefix=version_prefix,
|
||||||
error_format=error_format,
|
error_format=error_format,
|
||||||
|
unquote=unquote,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
)(handler)
|
)(handler)
|
||||||
return handler
|
return handler
|
||||||
@@ -286,7 +265,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **GET** *HTTP* method
|
Add an API URL under the **GET** *HTTP* method
|
||||||
@@ -329,7 +308,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **POST** *HTTP* method
|
Add an API URL under the **POST** *HTTP* method
|
||||||
@@ -372,7 +351,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PUT** *HTTP* method
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
@@ -415,7 +394,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **HEAD** *HTTP* method
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
@@ -466,7 +445,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **OPTIONS** *HTTP* method
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
@@ -517,7 +496,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PATCH** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
@@ -570,7 +549,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
ignore_body: bool = True,
|
ignore_body: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
) -> RouteHandler:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
@@ -614,7 +593,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate a function to be registered as a websocket route
|
Decorate a function to be registered as a websocket route
|
||||||
@@ -658,7 +637,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs: Any,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
A helper method to register a function as a websocket route.
|
A helper method to register a function as a websocket route.
|
||||||
@@ -693,317 +672,6 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
)(handler)
|
)(handler)
|
||||||
|
|
||||||
def static(
|
|
||||||
self,
|
|
||||||
uri,
|
|
||||||
file_or_directory: Union[str, bytes, PurePath],
|
|
||||||
pattern=r"/?.+",
|
|
||||||
use_modified_since=True,
|
|
||||||
use_content_range=False,
|
|
||||||
stream_large_files=False,
|
|
||||||
name="static",
|
|
||||||
host=None,
|
|
||||||
strict_slashes=None,
|
|
||||||
content_type=None,
|
|
||||||
apply=True,
|
|
||||||
resource_type=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Register a root to serve files from. The input can either be a
|
|
||||||
file or a directory. This method will enable an easy and simple way
|
|
||||||
to setup the :class:`Route` necessary to serve the static files.
|
|
||||||
|
|
||||||
:param uri: URL path to be used for serving static content
|
|
||||||
:param file_or_directory: Path for the Static file/directory with
|
|
||||||
static files
|
|
||||||
:param pattern: Regex Pattern identifying the valid static files
|
|
||||||
:param use_modified_since: If true, send file modified time, and return
|
|
||||||
not modified if the browser's matches the server's
|
|
||||||
:param use_content_range: If true, process header for range requests
|
|
||||||
and sends the file part that is requested
|
|
||||||
:param stream_large_files: If true, use the
|
|
||||||
:func:`StreamingHTTPResponse.file_stream` handler rather
|
|
||||||
than the :func:`HTTPResponse.file` handler to send the file.
|
|
||||||
If this is an integer, this represents the threshold size to
|
|
||||||
switch to :func:`StreamingHTTPResponse.file_stream`
|
|
||||||
:param name: user defined name used for url_for
|
|
||||||
:param host: Host IP or FQDN for the service to use
|
|
||||||
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
|
||||||
URLs need to terminate with a */*
|
|
||||||
:param content_type: user defined content type for header
|
|
||||||
:return: routes registered on the router
|
|
||||||
:rtype: List[sanic.router.Route]
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = self._generate_name(name)
|
|
||||||
|
|
||||||
if strict_slashes is None and self.strict_slashes is not None:
|
|
||||||
strict_slashes = self.strict_slashes
|
|
||||||
|
|
||||||
if not isinstance(file_or_directory, (str, bytes, PurePath)):
|
|
||||||
raise ValueError(
|
|
||||||
f"Static route must be a valid path, not {file_or_directory}"
|
|
||||||
)
|
|
||||||
|
|
||||||
static = FutureStatic(
|
|
||||||
uri,
|
|
||||||
file_or_directory,
|
|
||||||
pattern,
|
|
||||||
use_modified_since,
|
|
||||||
use_content_range,
|
|
||||||
stream_large_files,
|
|
||||||
name,
|
|
||||||
host,
|
|
||||||
strict_slashes,
|
|
||||||
content_type,
|
|
||||||
resource_type,
|
|
||||||
)
|
|
||||||
self._future_statics.add(static)
|
|
||||||
|
|
||||||
if apply:
|
|
||||||
self._apply_static(static)
|
|
||||||
|
|
||||||
def _generate_name(self, *objects) -> str:
|
|
||||||
name = None
|
|
||||||
|
|
||||||
for obj in objects:
|
|
||||||
if obj:
|
|
||||||
if isinstance(obj, str):
|
|
||||||
name = obj
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
name = obj.name
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
name = obj.__name__
|
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not name: # noqa
|
|
||||||
raise ValueError("Could not generate a name for handler")
|
|
||||||
|
|
||||||
if not name.startswith(f"{self.name}."):
|
|
||||||
name = f"{self.name}.{name}"
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
async def _static_request_handler(
|
|
||||||
self,
|
|
||||||
file_or_directory,
|
|
||||||
use_modified_since,
|
|
||||||
use_content_range,
|
|
||||||
stream_large_files,
|
|
||||||
request,
|
|
||||||
content_type=None,
|
|
||||||
__file_uri__=None,
|
|
||||||
):
|
|
||||||
# Merge served directory and requested file if provided
|
|
||||||
file_path_raw = Path(unquote(file_or_directory))
|
|
||||||
root_path = file_path = file_path_raw.resolve()
|
|
||||||
not_found = FileNotFound(
|
|
||||||
"File not found",
|
|
||||||
path=file_or_directory,
|
|
||||||
relative_url=__file_uri__,
|
|
||||||
)
|
|
||||||
|
|
||||||
if __file_uri__:
|
|
||||||
# Strip all / that in the beginning of the URL to help prevent
|
|
||||||
# python from herping a derp and treating the uri as an
|
|
||||||
# absolute path
|
|
||||||
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
|
|
||||||
file_path_raw = Path(file_or_directory, unquoted_file_uri)
|
|
||||||
file_path = file_path_raw.resolve()
|
|
||||||
if (
|
|
||||||
file_path < root_path and not file_path_raw.is_symlink()
|
|
||||||
) or ".." in file_path_raw.parts:
|
|
||||||
error_logger.exception(
|
|
||||||
f"File not found: path={file_or_directory}, "
|
|
||||||
f"relative_url={__file_uri__}"
|
|
||||||
)
|
|
||||||
raise not_found
|
|
||||||
|
|
||||||
try:
|
|
||||||
file_path.relative_to(root_path)
|
|
||||||
except ValueError:
|
|
||||||
if not file_path_raw.is_symlink():
|
|
||||||
error_logger.exception(
|
|
||||||
f"File not found: path={file_or_directory}, "
|
|
||||||
f"relative_url={__file_uri__}"
|
|
||||||
)
|
|
||||||
raise not_found
|
|
||||||
try:
|
|
||||||
headers = {}
|
|
||||||
# Check if the client has been sent this file before
|
|
||||||
# and it has not been modified since
|
|
||||||
stats = None
|
|
||||||
if use_modified_since:
|
|
||||||
stats = await stat_async(file_path)
|
|
||||||
modified_since = strftime(
|
|
||||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
request.headers.getone("if-modified-since", None)
|
|
||||||
== modified_since
|
|
||||||
):
|
|
||||||
return HTTPResponse(status=304)
|
|
||||||
headers["Last-Modified"] = modified_since
|
|
||||||
_range = None
|
|
||||||
if use_content_range:
|
|
||||||
_range = None
|
|
||||||
if not stats:
|
|
||||||
stats = await stat_async(file_path)
|
|
||||||
headers["Accept-Ranges"] = "bytes"
|
|
||||||
headers["Content-Length"] = str(stats.st_size)
|
|
||||||
if request.method != "HEAD":
|
|
||||||
try:
|
|
||||||
_range = ContentRangeHandler(request, stats)
|
|
||||||
except HeaderNotFound:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
del headers["Content-Length"]
|
|
||||||
for key, value in _range.headers.items():
|
|
||||||
headers[key] = value
|
|
||||||
|
|
||||||
if "content-type" not in headers:
|
|
||||||
content_type = (
|
|
||||||
content_type
|
|
||||||
or guess_type(file_path)[0]
|
|
||||||
or DEFAULT_HTTP_CONTENT_TYPE
|
|
||||||
)
|
|
||||||
|
|
||||||
if "charset=" not in content_type and (
|
|
||||||
content_type.startswith("text/")
|
|
||||||
or content_type == "application/javascript"
|
|
||||||
):
|
|
||||||
content_type += "; charset=utf-8"
|
|
||||||
|
|
||||||
headers["Content-Type"] = content_type
|
|
||||||
|
|
||||||
if request.method == "HEAD":
|
|
||||||
return HTTPResponse(headers=headers)
|
|
||||||
else:
|
|
||||||
if stream_large_files:
|
|
||||||
if type(stream_large_files) == int:
|
|
||||||
threshold = stream_large_files
|
|
||||||
else:
|
|
||||||
threshold = 1024 * 1024
|
|
||||||
|
|
||||||
if not stats:
|
|
||||||
stats = await stat_async(file_path)
|
|
||||||
if stats.st_size >= threshold:
|
|
||||||
return await file_stream(
|
|
||||||
file_path, headers=headers, _range=_range
|
|
||||||
)
|
|
||||||
return await file(file_path, headers=headers, _range=_range)
|
|
||||||
except RangeNotSatisfiable:
|
|
||||||
raise
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise not_found
|
|
||||||
except Exception:
|
|
||||||
error_logger.exception(
|
|
||||||
f"Exception in static request handler: "
|
|
||||||
f"path={file_or_directory}, "
|
|
||||||
f"relative_url={__file_uri__}"
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _register_static(
|
|
||||||
self,
|
|
||||||
static: FutureStatic,
|
|
||||||
):
|
|
||||||
# TODO: Though sanic is not a file server, I feel like we should
|
|
||||||
# at least make a good effort here. Modified-since is nice, but
|
|
||||||
# we could also look into etags, expires, and caching
|
|
||||||
"""
|
|
||||||
Register a static directory handler with Sanic by adding a route to the
|
|
||||||
router and registering a handler.
|
|
||||||
|
|
||||||
:param app: Sanic
|
|
||||||
:param file_or_directory: File or directory path to serve from
|
|
||||||
:type file_or_directory: Union[str,bytes,Path]
|
|
||||||
:param uri: URL to serve from
|
|
||||||
:type uri: str
|
|
||||||
:param pattern: regular expression used to match files in the URL
|
|
||||||
:param use_modified_since: If true, send file modified time, and return
|
|
||||||
not modified if the browser's matches the
|
|
||||||
server's
|
|
||||||
:param use_content_range: If true, process header for range requests
|
|
||||||
and sends the file part that is requested
|
|
||||||
:param stream_large_files: If true, use the file_stream() handler
|
|
||||||
rather than the file() handler to send the file
|
|
||||||
If this is an integer, this represents the
|
|
||||||
threshold size to switch to file_stream()
|
|
||||||
:param name: user defined name used for url_for
|
|
||||||
:type name: str
|
|
||||||
:param content_type: user defined content type for header
|
|
||||||
:return: registered static routes
|
|
||||||
:rtype: List[sanic.router.Route]
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(static.file_or_directory, bytes):
|
|
||||||
file_or_directory = static.file_or_directory.decode("utf-8")
|
|
||||||
elif isinstance(static.file_or_directory, PurePath):
|
|
||||||
file_or_directory = str(static.file_or_directory)
|
|
||||||
elif not isinstance(static.file_or_directory, str):
|
|
||||||
raise ValueError("Invalid file path string.")
|
|
||||||
else:
|
|
||||||
file_or_directory = static.file_or_directory
|
|
||||||
|
|
||||||
uri = static.uri
|
|
||||||
name = static.name
|
|
||||||
# If we're not trying to match a file directly,
|
|
||||||
# serve from the folder
|
|
||||||
if not static.resource_type:
|
|
||||||
if not path.isfile(file_or_directory):
|
|
||||||
uri += "/<__file_uri__:path>"
|
|
||||||
elif static.resource_type == "dir":
|
|
||||||
if path.isfile(file_or_directory):
|
|
||||||
raise TypeError(
|
|
||||||
"Resource type improperly identified as directory. "
|
|
||||||
f"'{file_or_directory}'"
|
|
||||||
)
|
|
||||||
uri += "/<__file_uri__:path>"
|
|
||||||
elif static.resource_type == "file" and not path.isfile(
|
|
||||||
file_or_directory
|
|
||||||
):
|
|
||||||
raise TypeError(
|
|
||||||
"Resource type improperly identified as file. "
|
|
||||||
f"'{file_or_directory}'"
|
|
||||||
)
|
|
||||||
elif static.resource_type != "file":
|
|
||||||
raise ValueError(
|
|
||||||
"The resource_type should be set to 'file' or 'dir'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# special prefix for static files
|
|
||||||
# if not static.name.startswith("_static_"):
|
|
||||||
# name = f"_static_{static.name}"
|
|
||||||
|
|
||||||
_handler = wraps(self._static_request_handler)(
|
|
||||||
partial(
|
|
||||||
self._static_request_handler,
|
|
||||||
file_or_directory,
|
|
||||||
static.use_modified_since,
|
|
||||||
static.use_content_range,
|
|
||||||
static.stream_large_files,
|
|
||||||
content_type=static.content_type,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
route, _ = self.route( # type: ignore
|
|
||||||
uri=uri,
|
|
||||||
methods=["GET", "HEAD"],
|
|
||||||
name=name,
|
|
||||||
host=static.host,
|
|
||||||
strict_slashes=static.strict_slashes,
|
|
||||||
static=True,
|
|
||||||
)(_handler)
|
|
||||||
|
|
||||||
return route
|
|
||||||
|
|
||||||
def _determine_error_format(self, handler) -> str:
|
def _determine_error_format(self, handler) -> str:
|
||||||
with suppress(OSError, TypeError):
|
with suppress(OSError, TypeError):
|
||||||
src = dedent(getsource(handler))
|
src = dedent(getsource(handler))
|
||||||
@@ -1039,24 +707,12 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
def _build_route_context(self, raw):
|
def _build_route_context(self, raw: Dict[str, Any]) -> HashableDict:
|
||||||
ctx_kwargs = {
|
ctx_kwargs = {
|
||||||
key.replace("ctx_", ""): raw.pop(key)
|
key.replace("ctx_", ""): raw.pop(key)
|
||||||
for key in {**raw}.keys()
|
for key in {**raw}.keys()
|
||||||
if key.startswith("ctx_")
|
if key.startswith("ctx_")
|
||||||
}
|
}
|
||||||
restricted = [
|
|
||||||
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
|
|
||||||
]
|
|
||||||
if restricted:
|
|
||||||
restricted_arguments = ", ".join(restricted)
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use restricted route context: "
|
|
||||||
f"{restricted_arguments}. This limitation is only in place "
|
|
||||||
"until v22.9 when the restricted names will no longer be in"
|
|
||||||
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
|
|
||||||
"for more information."
|
|
||||||
)
|
|
||||||
if raw:
|
if raw:
|
||||||
unexpected_arguments = ", ".join(raw.keys())
|
unexpected_arguments = ", ".join(raw.keys())
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
event: Union[str, Enum],
|
event: Union[str, Enum],
|
||||||
*,
|
*,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Optional[Dict[str, Any]] = None,
|
||||||
exclusive: bool = True,
|
exclusive: bool = True,
|
||||||
) -> Callable[[SignalHandler], SignalHandler]:
|
) -> Callable[[SignalHandler], SignalHandler]:
|
||||||
"""
|
"""
|
||||||
@@ -64,7 +64,7 @@ class SignalMixin(metaclass=SanicMeta):
|
|||||||
self,
|
self,
|
||||||
handler: Optional[Callable[..., Any]],
|
handler: Optional[Callable[..., Any]],
|
||||||
event: str,
|
event: str,
|
||||||
condition: Dict[str, Any] = None,
|
condition: Optional[Dict[str, Any]] = None,
|
||||||
exclusive: bool = True,
|
exclusive: bool = True,
|
||||||
):
|
):
|
||||||
if not handler:
|
if not handler:
|
||||||
|
|||||||
@@ -16,14 +16,18 @@ 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 Manager, Pipe, get_context
|
||||||
|
from multiprocessing.context import BaseContext
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socket import socket
|
from socket import SHUT_RDWR, socket
|
||||||
from ssl import SSLContext
|
from ssl import SSLContext
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
|
Callable,
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
@@ -32,24 +36,34 @@ from typing import (
|
|||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sanic import reloader_helpers
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.application.logo import get_logo
|
from sanic.application.logo import get_logo
|
||||||
from sanic.application.motd import MOTD
|
from sanic.application.motd import MOTD
|
||||||
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
from sanic.compat import OS_IS_WINDOWS, is_atty
|
from sanic.compat import OS_IS_WINDOWS, StartMethod, is_atty
|
||||||
from sanic.helpers import _default
|
from sanic.exceptions import ServerKilled
|
||||||
|
from sanic.helpers import Default, _default
|
||||||
from sanic.http.constants import HTTP
|
from sanic.http.constants import HTTP
|
||||||
from sanic.http.tls import get_ssl_context, process_to_context
|
from sanic.http.tls import get_ssl_context, process_to_context
|
||||||
|
from sanic.http.tls.context import SanicSSLContext
|
||||||
from sanic.log import Colors, deprecation, error_logger, logger
|
from sanic.log import Colors, deprecation, error_logger, logger
|
||||||
from sanic.models.handler_types import ListenerType
|
from sanic.models.handler_types import ListenerType
|
||||||
from sanic.server import Signal as ServerSignal
|
from sanic.server import Signal as ServerSignal
|
||||||
from sanic.server import try_use_uvloop
|
from sanic.server import try_use_uvloop
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
from sanic.server.events import trigger_events
|
from sanic.server.events import trigger_events
|
||||||
|
from sanic.server.legacy import watchdog
|
||||||
|
from sanic.server.loop import try_windows_loop
|
||||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
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, serve_multiple, serve_single
|
from sanic.server.runners import serve, serve_multiple, serve_single
|
||||||
|
from sanic.server.socket import configure_socket, remove_unix_socket
|
||||||
|
from sanic.worker.loader import AppLoader
|
||||||
|
from sanic.worker.manager import WorkerManager
|
||||||
|
from sanic.worker.multiplexer import WorkerMultiplexer
|
||||||
|
from sanic.worker.reloader import Reloader
|
||||||
|
from sanic.worker.serve import worker_serve
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -59,20 +73,37 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
|
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
|
||||||
|
|
||||||
if sys.version_info < (3, 8):
|
if sys.version_info < (3, 8): # no cov
|
||||||
HTTPVersion = Union[HTTP, int]
|
HTTPVersion = Union[HTTP, int]
|
||||||
else:
|
else: # no cov
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
HTTPVersion = Union[HTTP, Literal[1], Literal[3]]
|
HTTPVersion = Union[HTTP, Literal[1], Literal[3]]
|
||||||
|
|
||||||
|
|
||||||
class RunnerMixin(metaclass=SanicMeta):
|
class StartupMixin(metaclass=SanicMeta):
|
||||||
_app_registry: 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
|
||||||
|
start_method: StartMethod = _default
|
||||||
|
|
||||||
|
def setup_loop(self):
|
||||||
|
if not self.asgi:
|
||||||
|
if self.config.USE_UVLOOP is True or (
|
||||||
|
isinstance(self.config.USE_UVLOOP, Default)
|
||||||
|
and not OS_IS_WINDOWS
|
||||||
|
):
|
||||||
|
try_use_uvloop()
|
||||||
|
elif OS_IS_WINDOWS:
|
||||||
|
try_windows_loop()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def m(self) -> WorkerMultiplexer:
|
||||||
|
"""Interface for interacting with the worker processes"""
|
||||||
|
return self.multiplexer
|
||||||
|
|
||||||
def make_coffee(self, *args, **kwargs):
|
def make_coffee(self, *args, **kwargs):
|
||||||
self.state.coffee = True
|
self.state.coffee = True
|
||||||
@@ -95,7 +126,7 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
register_sys_signals: bool = True,
|
register_sys_signals: bool = True,
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
loop: AbstractEventLoop = None,
|
loop: Optional[AbstractEventLoop] = None,
|
||||||
reload_dir: Optional[Union[List[str], str]] = None,
|
reload_dir: Optional[Union[List[str], str]] = None,
|
||||||
noisy_exceptions: Optional[bool] = None,
|
noisy_exceptions: Optional[bool] = None,
|
||||||
motd: bool = True,
|
motd: bool = True,
|
||||||
@@ -103,6 +134,8 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
verbosity: int = 0,
|
verbosity: int = 0,
|
||||||
motd_display: Optional[Dict[str, str]] = None,
|
motd_display: Optional[Dict[str, str]] = None,
|
||||||
auto_tls: bool = False,
|
auto_tls: bool = False,
|
||||||
|
single_process: bool = False,
|
||||||
|
legacy: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run the HTTP Server and listen until keyboard interrupt or term
|
Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
@@ -163,9 +196,17 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
verbosity=verbosity,
|
verbosity=verbosity,
|
||||||
motd_display=motd_display,
|
motd_display=motd_display,
|
||||||
auto_tls=auto_tls,
|
auto_tls=auto_tls,
|
||||||
|
single_process=single_process,
|
||||||
|
legacy=legacy,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__class__.serve(primary=self) # type: ignore
|
if single_process:
|
||||||
|
serve = self.__class__.serve_single
|
||||||
|
elif legacy:
|
||||||
|
serve = self.__class__.serve_legacy
|
||||||
|
else:
|
||||||
|
serve = self.__class__.serve
|
||||||
|
serve(primary=self) # type: ignore
|
||||||
|
|
||||||
def prepare(
|
def prepare(
|
||||||
self,
|
self,
|
||||||
@@ -184,14 +225,17 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
register_sys_signals: bool = True,
|
register_sys_signals: bool = True,
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
loop: AbstractEventLoop = None,
|
loop: Optional[AbstractEventLoop] = None,
|
||||||
reload_dir: Optional[Union[List[str], str]] = None,
|
reload_dir: Optional[Union[List[str], str]] = None,
|
||||||
noisy_exceptions: Optional[bool] = None,
|
noisy_exceptions: Optional[bool] = None,
|
||||||
motd: bool = True,
|
motd: bool = True,
|
||||||
fast: bool = False,
|
fast: bool = False,
|
||||||
verbosity: int = 0,
|
verbosity: int = 0,
|
||||||
motd_display: Optional[Dict[str, str]] = None,
|
motd_display: Optional[Dict[str, str]] = None,
|
||||||
|
coffee: bool = False,
|
||||||
auto_tls: bool = False,
|
auto_tls: bool = False,
|
||||||
|
single_process: bool = False,
|
||||||
|
legacy: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if version == 3 and self.state.server_info:
|
if version == 3 and self.state.server_info:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -204,6 +248,9 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
debug = True
|
debug = True
|
||||||
auto_reload = True
|
auto_reload = True
|
||||||
|
|
||||||
|
if debug and access_log is None:
|
||||||
|
access_log = True
|
||||||
|
|
||||||
self.state.verbosity = verbosity
|
self.state.verbosity = verbosity
|
||||||
if not self.state.auto_reload:
|
if not self.state.auto_reload:
|
||||||
self.state.auto_reload = bool(auto_reload)
|
self.state.auto_reload = bool(auto_reload)
|
||||||
@@ -211,6 +258,21 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
if fast and workers != 1:
|
if fast and workers != 1:
|
||||||
raise RuntimeError("You cannot use both fast=True and workers=X")
|
raise RuntimeError("You cannot use both fast=True and workers=X")
|
||||||
|
|
||||||
|
if single_process and (fast or (workers > 1) or auto_reload):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Single process cannot be run with multiple workers "
|
||||||
|
"or auto-reload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if single_process and legacy:
|
||||||
|
raise RuntimeError("Cannot run single process and legacy mode")
|
||||||
|
|
||||||
|
# if register_sys_signals is False and not (single_process or legacy):
|
||||||
|
# raise RuntimeError(
|
||||||
|
# "Cannot run Sanic.serve with register_sys_signals=False. "
|
||||||
|
# "Use either Sanic.serve_single or Sanic.serve_legacy."
|
||||||
|
# )
|
||||||
|
|
||||||
if motd_display:
|
if motd_display:
|
||||||
self.config.MOTD_DISPLAY.update(motd_display)
|
self.config.MOTD_DISPLAY.update(motd_display)
|
||||||
|
|
||||||
@@ -234,12 +296,6 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
"#asynchronous-support"
|
"#asynchronous-support"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
self.__class__.should_auto_reload()
|
|
||||||
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
|
|
||||||
): # no cov
|
|
||||||
return
|
|
||||||
|
|
||||||
if sock is None:
|
if sock is None:
|
||||||
host, port = self.get_address(host, port, version, auto_tls)
|
host, port = self.get_address(host, port, version, auto_tls)
|
||||||
|
|
||||||
@@ -265,6 +321,9 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
except AttributeError: # no cov
|
except AttributeError: # no cov
|
||||||
workers = os.cpu_count() or 1
|
workers = os.cpu_count() or 1
|
||||||
|
|
||||||
|
if coffee:
|
||||||
|
self.state.coffee = True
|
||||||
|
|
||||||
server_settings = self._helper(
|
server_settings = self._helper(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
@@ -283,10 +342,10 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
ApplicationServerInfo(settings=server_settings)
|
ApplicationServerInfo(settings=server_settings)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.config.USE_UVLOOP is True or (
|
# if self.config.USE_UVLOOP is True or (
|
||||||
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
|
# self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
|
||||||
):
|
# ):
|
||||||
try_use_uvloop()
|
# try_use_uvloop()
|
||||||
|
|
||||||
async def create_server(
|
async def create_server(
|
||||||
self,
|
self,
|
||||||
@@ -296,12 +355,12 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock: Optional[socket] = None,
|
sock: Optional[socket] = None,
|
||||||
protocol: Type[Protocol] = None,
|
protocol: Optional[Type[Protocol]] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
return_asyncio_server: bool = False,
|
return_asyncio_server: bool = False,
|
||||||
asyncio_server_kwargs: Dict[str, Any] = None,
|
asyncio_server_kwargs: Optional[Dict[str, Any]] = None,
|
||||||
noisy_exceptions: Optional[bool] = None,
|
noisy_exceptions: Optional[bool] = None,
|
||||||
) -> Optional[AsyncioServer]:
|
) -> Optional[AsyncioServer]:
|
||||||
"""
|
"""
|
||||||
@@ -375,7 +434,7 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
run_async=return_asyncio_server,
|
run_async=return_asyncio_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.config.USE_UVLOOP is not _default:
|
if not isinstance(self.config.USE_UVLOOP, Default):
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
"You are trying to change the uvloop configuration, but "
|
"You are trying to change the uvloop configuration, but "
|
||||||
"this is only effective when using the run(...) method. "
|
"this is only effective when using the run(...) method. "
|
||||||
@@ -395,18 +454,23 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
||||||
)
|
)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self, terminate: bool = True, unregister: bool = False):
|
||||||
"""
|
"""
|
||||||
This kills the Sanic
|
This kills the Sanic
|
||||||
"""
|
"""
|
||||||
|
if terminate and hasattr(self, "multiplexer"):
|
||||||
|
self.multiplexer.terminate()
|
||||||
if self.state.stage is not ServerStage.STOPPED:
|
if self.state.stage is not ServerStage.STOPPED:
|
||||||
self.shutdown_tasks(timeout=0)
|
self.shutdown_tasks(timeout=0) # type: ignore
|
||||||
for task in all_tasks():
|
for task in all_tasks():
|
||||||
with suppress(AttributeError):
|
with suppress(AttributeError):
|
||||||
if task.get_name() == "RunServer":
|
if task.get_name() == "RunServer":
|
||||||
task.cancel()
|
task.cancel()
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
|
|
||||||
|
if unregister:
|
||||||
|
self.__class__.unregister_app(self) # type: ignore
|
||||||
|
|
||||||
def _helper(
|
def _helper(
|
||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
@@ -417,7 +481,7 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
sock: Optional[socket] = None,
|
sock: Optional[socket] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
loop: AbstractEventLoop = None,
|
loop: Optional[AbstractEventLoop] = None,
|
||||||
protocol: Type[Protocol] = HttpProtocol,
|
protocol: Type[Protocol] = HttpProtocol,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
register_sys_signals: bool = True,
|
register_sys_signals: bool = True,
|
||||||
@@ -468,7 +532,11 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
self.motd(server_settings=server_settings)
|
self.motd(server_settings=server_settings)
|
||||||
|
|
||||||
if is_atty() and not self.state.is_debug:
|
if (
|
||||||
|
is_atty()
|
||||||
|
and not self.state.is_debug
|
||||||
|
and not os.environ.get("SANIC_IGNORE_PRODUCTION_WARNING")
|
||||||
|
):
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
|
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
|
||||||
"Consider using '--debug' or '--dev' while actively "
|
"Consider using '--debug' or '--dev' while actively "
|
||||||
@@ -494,85 +562,94 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
def motd(
|
def motd(
|
||||||
self,
|
self,
|
||||||
serve_location: str = "",
|
|
||||||
server_settings: Optional[Dict[str, Any]] = None,
|
server_settings: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
if serve_location:
|
if (
|
||||||
deprecation(
|
os.environ.get("SANIC_WORKER_NAME")
|
||||||
"Specifying a serve_location in the MOTD is deprecated and "
|
or os.environ.get("SANIC_MOTD_OUTPUT")
|
||||||
"will be removed.",
|
or os.environ.get("SANIC_WORKER_PROCESS")
|
||||||
22.9,
|
or os.environ.get("SANIC_SERVER_RUNNING")
|
||||||
)
|
):
|
||||||
else:
|
return
|
||||||
serve_location = self.get_server_location(server_settings)
|
serve_location = self.get_server_location(server_settings)
|
||||||
if self.config.MOTD:
|
if self.config.MOTD:
|
||||||
mode = [f"{self.state.mode},"]
|
|
||||||
if self.state.fast:
|
|
||||||
mode.append("goin' fast")
|
|
||||||
if self.state.asgi:
|
|
||||||
mode.append("ASGI")
|
|
||||||
else:
|
|
||||||
if self.state.workers == 1:
|
|
||||||
mode.append("single worker")
|
|
||||||
else:
|
|
||||||
mode.append(f"w/ {self.state.workers} workers")
|
|
||||||
|
|
||||||
if server_settings:
|
|
||||||
server = ", ".join(
|
|
||||||
(
|
|
||||||
self.state.server,
|
|
||||||
server_settings["version"].display(), # type: ignore
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
server = ""
|
|
||||||
|
|
||||||
display = {
|
|
||||||
"mode": " ".join(mode),
|
|
||||||
"server": server,
|
|
||||||
"python": platform.python_version(),
|
|
||||||
"platform": platform.platform(),
|
|
||||||
}
|
|
||||||
extra = {}
|
|
||||||
if self.config.AUTO_RELOAD:
|
|
||||||
reload_display = "enabled"
|
|
||||||
if self.state.reload_dirs:
|
|
||||||
reload_display += ", ".join(
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
*(
|
|
||||||
str(path.absolute())
|
|
||||||
for path in self.state.reload_dirs
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
display["auto-reload"] = reload_display
|
|
||||||
|
|
||||||
packages = []
|
|
||||||
for package_name in SANIC_PACKAGES:
|
|
||||||
module_name = package_name.replace("-", "_")
|
|
||||||
try:
|
|
||||||
module = import_module(module_name)
|
|
||||||
packages.append(
|
|
||||||
f"{package_name}=={module.__version__}" # type: ignore
|
|
||||||
)
|
|
||||||
except ImportError:
|
|
||||||
...
|
|
||||||
|
|
||||||
if packages:
|
|
||||||
display["packages"] = ", ".join(packages)
|
|
||||||
|
|
||||||
if self.config.MOTD_DISPLAY:
|
|
||||||
extra.update(self.config.MOTD_DISPLAY)
|
|
||||||
|
|
||||||
logo = get_logo(coffee=self.state.coffee)
|
logo = get_logo(coffee=self.state.coffee)
|
||||||
|
display, extra = self.get_motd_data(server_settings)
|
||||||
|
|
||||||
MOTD.output(logo, serve_location, display, extra)
|
MOTD.output(logo, serve_location, display, extra)
|
||||||
|
|
||||||
|
def get_motd_data(
|
||||||
|
self, server_settings: Optional[Dict[str, Any]] = None
|
||||||
|
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
||||||
|
mode = [f"{self.state.mode},"]
|
||||||
|
if self.state.fast:
|
||||||
|
mode.append("goin' fast")
|
||||||
|
if self.state.asgi:
|
||||||
|
mode.append("ASGI")
|
||||||
|
else:
|
||||||
|
if self.state.workers == 1:
|
||||||
|
mode.append("single worker")
|
||||||
|
else:
|
||||||
|
mode.append(f"w/ {self.state.workers} workers")
|
||||||
|
|
||||||
|
if server_settings:
|
||||||
|
server = ", ".join(
|
||||||
|
(
|
||||||
|
self.state.server,
|
||||||
|
server_settings["version"].display(), # type: ignore
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
server = "ASGI" if self.asgi else "unknown" # type: ignore
|
||||||
|
|
||||||
|
display = {
|
||||||
|
"mode": " ".join(mode),
|
||||||
|
"server": server,
|
||||||
|
"python": platform.python_version(),
|
||||||
|
"platform": platform.platform(),
|
||||||
|
}
|
||||||
|
extra = {}
|
||||||
|
if self.config.AUTO_RELOAD:
|
||||||
|
reload_display = "enabled"
|
||||||
|
if self.state.reload_dirs:
|
||||||
|
reload_display += ", ".join(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
*(
|
||||||
|
str(path.absolute())
|
||||||
|
for path in self.state.reload_dirs
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
display["auto-reload"] = reload_display
|
||||||
|
|
||||||
|
packages = []
|
||||||
|
for package_name in SANIC_PACKAGES:
|
||||||
|
module_name = package_name.replace("-", "_")
|
||||||
|
try:
|
||||||
|
module = import_module(module_name)
|
||||||
|
packages.append(
|
||||||
|
f"{package_name}=={module.__version__}" # type: ignore
|
||||||
|
)
|
||||||
|
except ImportError: # no cov
|
||||||
|
...
|
||||||
|
|
||||||
|
if packages:
|
||||||
|
display["packages"] = ", ".join(packages)
|
||||||
|
|
||||||
|
if self.config.MOTD_DISPLAY:
|
||||||
|
extra.update(self.config.MOTD_DISPLAY)
|
||||||
|
|
||||||
|
return display, extra
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serve_location(self) -> str:
|
def serve_location(self) -> str:
|
||||||
server_settings = self.state.server_info[0].settings
|
try:
|
||||||
return self.get_server_location(server_settings)
|
server_settings = self.state.server_info[0].settings
|
||||||
|
return self.get_server_location(server_settings)
|
||||||
|
except IndexError:
|
||||||
|
location = "ASGI" if self.asgi else "unknown" # type: ignore
|
||||||
|
return f"http://<{location}>"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_server_location(
|
def get_server_location(
|
||||||
@@ -583,24 +660,20 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
if not server_settings:
|
if not server_settings:
|
||||||
return serve_location
|
return serve_location
|
||||||
|
|
||||||
if server_settings["ssl"] is not None:
|
host = server_settings["host"]
|
||||||
|
port = server_settings["port"]
|
||||||
|
|
||||||
|
if server_settings.get("ssl") is not None:
|
||||||
proto = "https"
|
proto = "https"
|
||||||
if server_settings["unix"]:
|
if server_settings.get("unix"):
|
||||||
serve_location = f'{server_settings["unix"]} {proto}://...'
|
serve_location = f'{server_settings["unix"]} {proto}://...'
|
||||||
elif server_settings["sock"]:
|
elif server_settings.get("sock"):
|
||||||
serve_location = (
|
host, port, *_ = server_settings["sock"].getsockname()
|
||||||
f'{server_settings["sock"].getsockname()} {proto}://...'
|
|
||||||
)
|
if not serve_location and host and port:
|
||||||
elif server_settings["host"] and server_settings["port"]:
|
|
||||||
# colon(:) is legal for a host only in an ipv6 address
|
# colon(:) is legal for a host only in an ipv6 address
|
||||||
display_host = (
|
display_host = f"[{host}]" if ":" in host else host
|
||||||
f'[{server_settings["host"]}]'
|
serve_location = f"{proto}://{display_host}:{port}"
|
||||||
if ":" in server_settings["host"]
|
|
||||||
else server_settings["host"]
|
|
||||||
)
|
|
||||||
serve_location = (
|
|
||||||
f'{proto}://{display_host}:{server_settings["port"]}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return serve_location
|
return serve_location
|
||||||
|
|
||||||
@@ -620,7 +693,268 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
return any(app.state.auto_reload for app in cls._app_registry.values())
|
return any(app.state.auto_reload for app in cls._app_registry.values())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serve(cls, primary: Optional[Sanic] = None) -> None:
|
def _get_startup_method(cls) -> str:
|
||||||
|
return (
|
||||||
|
cls.start_method
|
||||||
|
if not isinstance(cls.start_method, Default)
|
||||||
|
else "spawn"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_context(cls) -> BaseContext:
|
||||||
|
method = cls._get_startup_method()
|
||||||
|
logger.debug("Creating multiprocessing context using '%s'", method)
|
||||||
|
return get_context(method)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def serve(
|
||||||
|
cls,
|
||||||
|
primary: Optional[Sanic] = None,
|
||||||
|
*,
|
||||||
|
app_loader: Optional[AppLoader] = None,
|
||||||
|
factory: Optional[Callable[[], Sanic]] = None,
|
||||||
|
) -> None:
|
||||||
|
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
||||||
|
apps = list(cls._app_registry.values())
|
||||||
|
if factory:
|
||||||
|
primary = factory()
|
||||||
|
else:
|
||||||
|
if not primary:
|
||||||
|
if app_loader:
|
||||||
|
primary = app_loader.load()
|
||||||
|
if not primary:
|
||||||
|
try:
|
||||||
|
primary = apps[0]
|
||||||
|
except IndexError:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Did not find any applications."
|
||||||
|
) from None
|
||||||
|
|
||||||
|
# This exists primarily for unit testing
|
||||||
|
if not primary.state.server_info: # no cov
|
||||||
|
for app in apps:
|
||||||
|
app.state.server_info.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
primary_server_info = primary.state.server_info[0]
|
||||||
|
except IndexError:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"No server information found for {primary.name}. Perhaps you "
|
||||||
|
"need to run app.prepare(...)?"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
socks = []
|
||||||
|
sync_manager = Manager()
|
||||||
|
setup_ext(primary)
|
||||||
|
exit_code = 0
|
||||||
|
try:
|
||||||
|
primary_server_info.settings.pop("main_start", None)
|
||||||
|
primary_server_info.settings.pop("main_stop", None)
|
||||||
|
main_start = primary.listeners.get("main_process_start")
|
||||||
|
main_stop = primary.listeners.get("main_process_stop")
|
||||||
|
app = primary_server_info.settings.pop("app")
|
||||||
|
app.setup_loop()
|
||||||
|
loop = new_event_loop()
|
||||||
|
trigger_events(main_start, loop, primary)
|
||||||
|
|
||||||
|
socks = [
|
||||||
|
sock
|
||||||
|
for sock in [
|
||||||
|
configure_socket(server_info.settings)
|
||||||
|
for app in apps
|
||||||
|
for server_info in app.state.server_info
|
||||||
|
]
|
||||||
|
if sock
|
||||||
|
]
|
||||||
|
primary_server_info.settings["run_multiple"] = True
|
||||||
|
monitor_sub, monitor_pub = Pipe(True)
|
||||||
|
worker_state: Mapping[str, Any] = sync_manager.dict()
|
||||||
|
kwargs: Dict[str, Any] = {
|
||||||
|
**primary_server_info.settings,
|
||||||
|
"monitor_publisher": monitor_pub,
|
||||||
|
"worker_state": worker_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not app_loader:
|
||||||
|
if factory:
|
||||||
|
app_loader = AppLoader(factory=factory)
|
||||||
|
else:
|
||||||
|
app_loader = AppLoader(
|
||||||
|
factory=partial(cls.get_app, app.name) # type: ignore
|
||||||
|
)
|
||||||
|
kwargs["app_name"] = app.name
|
||||||
|
kwargs["app_loader"] = app_loader
|
||||||
|
kwargs["server_info"] = {}
|
||||||
|
kwargs["passthru"] = {
|
||||||
|
"auto_reload": app.auto_reload,
|
||||||
|
"state": {
|
||||||
|
"verbosity": app.state.verbosity,
|
||||||
|
"mode": app.state.mode,
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"ACCESS_LOG": app.config.ACCESS_LOG,
|
||||||
|
"NOISY_EXCEPTIONS": app.config.NOISY_EXCEPTIONS,
|
||||||
|
},
|
||||||
|
"shared_ctx": app.shared_ctx.__dict__,
|
||||||
|
}
|
||||||
|
for app in apps:
|
||||||
|
kwargs["server_info"][app.name] = []
|
||||||
|
for server_info in app.state.server_info:
|
||||||
|
server_info.settings = {
|
||||||
|
k: v
|
||||||
|
for k, v in server_info.settings.items()
|
||||||
|
if k not in ("main_start", "main_stop", "app", "ssl")
|
||||||
|
}
|
||||||
|
kwargs["server_info"][app.name].append(server_info)
|
||||||
|
|
||||||
|
ssl = kwargs.get("ssl")
|
||||||
|
|
||||||
|
if isinstance(ssl, SanicSSLContext):
|
||||||
|
kwargs["ssl"] = kwargs["ssl"].sanic
|
||||||
|
|
||||||
|
manager = WorkerManager(
|
||||||
|
primary.state.workers,
|
||||||
|
worker_serve,
|
||||||
|
kwargs,
|
||||||
|
cls._get_context(),
|
||||||
|
(monitor_pub, monitor_sub),
|
||||||
|
worker_state,
|
||||||
|
)
|
||||||
|
if cls.should_auto_reload():
|
||||||
|
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
|
||||||
|
*(app.state.reload_dirs for app in apps)
|
||||||
|
)
|
||||||
|
reloader = Reloader(monitor_pub, 0, reload_dirs, app_loader)
|
||||||
|
manager.manage("Reloader", reloader, {}, transient=False)
|
||||||
|
|
||||||
|
inspector = None
|
||||||
|
if primary.config.INSPECTOR:
|
||||||
|
display, extra = primary.get_motd_data()
|
||||||
|
packages = [
|
||||||
|
pkg.strip() for pkg in display["packages"].split(",")
|
||||||
|
]
|
||||||
|
module = import_module("sanic")
|
||||||
|
sanic_version = f"sanic=={module.__version__}" # type: ignore
|
||||||
|
app_info = {
|
||||||
|
**display,
|
||||||
|
"packages": [sanic_version, *packages],
|
||||||
|
"extra": extra,
|
||||||
|
}
|
||||||
|
inspector = primary.inspector_class(
|
||||||
|
monitor_pub,
|
||||||
|
app_info,
|
||||||
|
worker_state,
|
||||||
|
primary.config.INSPECTOR_HOST,
|
||||||
|
primary.config.INSPECTOR_PORT,
|
||||||
|
primary.config.INSPECTOR_API_KEY,
|
||||||
|
primary.config.INSPECTOR_TLS_KEY,
|
||||||
|
primary.config.INSPECTOR_TLS_CERT,
|
||||||
|
)
|
||||||
|
manager.manage("Inspector", inspector, {}, transient=False)
|
||||||
|
|
||||||
|
primary._inspector = inspector
|
||||||
|
primary._manager = manager
|
||||||
|
|
||||||
|
ready = primary.listeners["main_process_ready"]
|
||||||
|
trigger_events(ready, loop, primary)
|
||||||
|
|
||||||
|
manager.run()
|
||||||
|
except ServerKilled:
|
||||||
|
exit_code = 1
|
||||||
|
except BaseException:
|
||||||
|
kwargs = primary_server_info.settings
|
||||||
|
error_logger.exception(
|
||||||
|
"Experienced exception while trying to serve"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
logger.info("Server Stopped")
|
||||||
|
for app in apps:
|
||||||
|
app.state.server_info.clear()
|
||||||
|
app.router.reset()
|
||||||
|
app.signal_router.reset()
|
||||||
|
|
||||||
|
sync_manager.shutdown()
|
||||||
|
for sock in socks:
|
||||||
|
sock.shutdown(SHUT_RDWR)
|
||||||
|
sock.close()
|
||||||
|
socks = []
|
||||||
|
trigger_events(main_stop, loop, primary)
|
||||||
|
loop.close()
|
||||||
|
cls._cleanup_env_vars()
|
||||||
|
cls._cleanup_apps()
|
||||||
|
unix = kwargs.get("unix")
|
||||||
|
if unix:
|
||||||
|
remove_unix_socket(unix)
|
||||||
|
if exit_code:
|
||||||
|
os._exit(exit_code)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def serve_single(cls, primary: Optional[Sanic] = None) -> None:
|
||||||
|
os.environ["SANIC_MOTD_OUTPUT"] = "true"
|
||||||
|
apps = list(cls._app_registry.values())
|
||||||
|
|
||||||
|
if not primary:
|
||||||
|
try:
|
||||||
|
primary = apps[0]
|
||||||
|
except IndexError:
|
||||||
|
raise RuntimeError("Did not find any applications.")
|
||||||
|
|
||||||
|
# This exists primarily for unit testing
|
||||||
|
if not primary.state.server_info: # no cov
|
||||||
|
for app in apps:
|
||||||
|
app.state.server_info.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
primary_server_info = primary.state.server_info[0]
|
||||||
|
primary.before_server_start(partial(primary._start_servers, apps=apps))
|
||||||
|
kwargs = {
|
||||||
|
k: v
|
||||||
|
for k, v in primary_server_info.settings.items()
|
||||||
|
if k
|
||||||
|
not in (
|
||||||
|
"main_start",
|
||||||
|
"main_stop",
|
||||||
|
"app",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
kwargs["app_name"] = primary.name
|
||||||
|
kwargs["app_loader"] = None
|
||||||
|
sock = configure_socket(kwargs)
|
||||||
|
|
||||||
|
kwargs["server_info"] = {}
|
||||||
|
kwargs["server_info"][primary.name] = []
|
||||||
|
for server_info in primary.state.server_info:
|
||||||
|
server_info.settings = {
|
||||||
|
k: v
|
||||||
|
for k, v in server_info.settings.items()
|
||||||
|
if k not in ("main_start", "main_stop", "app")
|
||||||
|
}
|
||||||
|
kwargs["server_info"][primary.name].append(server_info)
|
||||||
|
|
||||||
|
try:
|
||||||
|
worker_serve(monitor_publisher=None, **kwargs)
|
||||||
|
except BaseException:
|
||||||
|
error_logger.exception(
|
||||||
|
"Experienced exception while trying to serve"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
logger.info("Server Stopped")
|
||||||
|
for app in apps:
|
||||||
|
app.state.server_info.clear()
|
||||||
|
app.router.reset()
|
||||||
|
app.signal_router.reset()
|
||||||
|
|
||||||
|
if sock:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
cls._cleanup_env_vars()
|
||||||
|
cls._cleanup_apps()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def serve_legacy(cls, primary: Optional[Sanic] = None) -> None:
|
||||||
apps = list(cls._app_registry.values())
|
apps = list(cls._app_registry.values())
|
||||||
|
|
||||||
if not primary:
|
if not primary:
|
||||||
@@ -641,7 +975,7 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
|
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
|
||||||
*(app.state.reload_dirs for app in apps)
|
*(app.state.reload_dirs for app in apps)
|
||||||
)
|
)
|
||||||
reloader_helpers.watchdog(1.0, reload_dirs)
|
watchdog(1.0, reload_dirs)
|
||||||
trigger_events(reloader_stop, loop, primary)
|
trigger_events(reloader_stop, loop, primary)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -654,11 +988,17 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
primary_server_info = primary.state.server_info[0]
|
primary_server_info = primary.state.server_info[0]
|
||||||
primary.before_server_start(partial(primary._start_servers, apps=apps))
|
primary.before_server_start(partial(primary._start_servers, apps=apps))
|
||||||
|
|
||||||
|
deprecation(
|
||||||
|
f"{Colors.YELLOW}Running {Colors.SANIC}Sanic {Colors.YELLOW}w/ "
|
||||||
|
f"LEGACY manager.{Colors.END} Support for will be dropped in "
|
||||||
|
"version 23.3.",
|
||||||
|
23.3,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
primary_server_info.stage = ServerStage.SERVING
|
primary_server_info.stage = ServerStage.SERVING
|
||||||
|
|
||||||
if primary.state.workers > 1 and os.name != "posix": # no cov
|
if primary.state.workers > 1 and os.name != "posix": # no cov
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f"Multiprocessing is currently not supported on {os.name},"
|
f"Multiprocessing is currently not supported on {os.name},"
|
||||||
" using workers=1 instead"
|
" using workers=1 instead"
|
||||||
)
|
)
|
||||||
@@ -679,10 +1019,9 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
finally:
|
finally:
|
||||||
primary_server_info.stage = ServerStage.STOPPED
|
primary_server_info.stage = ServerStage.STOPPED
|
||||||
logger.info("Server Stopped")
|
logger.info("Server Stopped")
|
||||||
for app in apps:
|
|
||||||
app.state.server_info.clear()
|
cls._cleanup_env_vars()
|
||||||
app.router.reset()
|
cls._cleanup_apps()
|
||||||
app.signal_router.reset()
|
|
||||||
|
|
||||||
async def _start_servers(
|
async def _start_servers(
|
||||||
self,
|
self,
|
||||||
@@ -720,7 +1059,7 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
*server_info.settings.pop("main_start", []),
|
*server_info.settings.pop("main_start", []),
|
||||||
*server_info.settings.pop("main_stop", []),
|
*server_info.settings.pop("main_stop", []),
|
||||||
]
|
]
|
||||||
if handlers:
|
if handlers: # no cov
|
||||||
error_logger.warning(
|
error_logger.warning(
|
||||||
f"Sanic found {len(handlers)} listener(s) on "
|
f"Sanic found {len(handlers)} listener(s) on "
|
||||||
"secondary applications attached to the main "
|
"secondary applications attached to the main "
|
||||||
@@ -733,12 +1072,15 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
if not server_info.settings["loop"]:
|
if not server_info.settings["loop"]:
|
||||||
server_info.settings["loop"] = get_running_loop()
|
server_info.settings["loop"] = get_running_loop()
|
||||||
|
|
||||||
|
serve_args: Dict[str, Any] = {
|
||||||
|
**server_info.settings,
|
||||||
|
"run_async": True,
|
||||||
|
"reuse_port": bool(primary.state.workers - 1),
|
||||||
|
}
|
||||||
|
if "app" not in serve_args:
|
||||||
|
serve_args["app"] = app
|
||||||
try:
|
try:
|
||||||
server_info.server = await serve(
|
server_info.server = await serve(**serve_args)
|
||||||
**server_info.settings,
|
|
||||||
run_async=True,
|
|
||||||
reuse_port=bool(primary.state.workers - 1),
|
|
||||||
)
|
|
||||||
except OSError as e: # no cov
|
except OSError as e: # no cov
|
||||||
first_message = (
|
first_message = (
|
||||||
"An OSError was detected on startup. "
|
"An OSError was detected on startup. "
|
||||||
@@ -764,10 +1106,9 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
async def _run_server(
|
async def _run_server(
|
||||||
self,
|
self,
|
||||||
app: RunnerMixin,
|
app: StartupMixin,
|
||||||
server_info: ApplicationServerInfo,
|
server_info: ApplicationServerInfo,
|
||||||
) -> None:
|
) -> None: # no cov
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# We should never get to this point without a server
|
# We should never get to this point without a server
|
||||||
# This is primarily to keep mypy happy
|
# This is primarily to keep mypy happy
|
||||||
@@ -790,3 +1131,26 @@ class RunnerMixin(metaclass=SanicMeta):
|
|||||||
finally:
|
finally:
|
||||||
server_info.stage = ServerStage.STOPPED
|
server_info.stage = ServerStage.STOPPED
|
||||||
server_info.server = None
|
server_info.server = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cleanup_env_vars():
|
||||||
|
variables = (
|
||||||
|
"SANIC_RELOADER_PROCESS",
|
||||||
|
"SANIC_IGNORE_PRODUCTION_WARNING",
|
||||||
|
"SANIC_WORKER_NAME",
|
||||||
|
"SANIC_MOTD_OUTPUT",
|
||||||
|
"SANIC_WORKER_PROCESS",
|
||||||
|
"SANIC_SERVER_RUNNING",
|
||||||
|
)
|
||||||
|
for var in variables:
|
||||||
|
try:
|
||||||
|
del os.environ[var]
|
||||||
|
except KeyError:
|
||||||
|
...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cleanup_apps(cls):
|
||||||
|
for app in cls._app_registry.values():
|
||||||
|
app.state.server_info.clear()
|
||||||
|
app.router.reset()
|
||||||
|
app.signal_router.reset()
|
||||||
348
sanic/mixins/static.py
Normal file
348
sanic/mixins/static.py
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
from email.utils import formatdate
|
||||||
|
from functools import partial, wraps
|
||||||
|
from mimetypes import guess_type
|
||||||
|
from os import PathLike, path
|
||||||
|
from pathlib import Path, PurePath
|
||||||
|
from typing import Optional, Sequence, Set, Union, cast
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from sanic_routing.route import Route
|
||||||
|
|
||||||
|
from sanic.base.meta import SanicMeta
|
||||||
|
from sanic.compat import stat_async
|
||||||
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||||
|
from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable
|
||||||
|
from sanic.handlers import ContentRangeHandler
|
||||||
|
from sanic.handlers.directory import DirectoryHandler
|
||||||
|
from sanic.log import deprecation, error_logger
|
||||||
|
from sanic.mixins.base import BaseMixin
|
||||||
|
from sanic.models.futures import FutureStatic
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import HTTPResponse, file, file_stream, validate_file
|
||||||
|
|
||||||
|
|
||||||
|
class StaticMixin(BaseMixin, metaclass=SanicMeta):
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
self._future_statics: Set[FutureStatic] = set()
|
||||||
|
|
||||||
|
def _apply_static(self, static: FutureStatic) -> Route:
|
||||||
|
raise NotImplementedError # noqa
|
||||||
|
|
||||||
|
def static(
|
||||||
|
self,
|
||||||
|
uri: str,
|
||||||
|
file_or_directory: Union[PathLike, str, bytes],
|
||||||
|
pattern: str = r"/?.+",
|
||||||
|
use_modified_since: bool = True,
|
||||||
|
use_content_range: bool = False,
|
||||||
|
stream_large_files: Union[bool, int] = False,
|
||||||
|
name: str = "static",
|
||||||
|
host: Optional[str] = None,
|
||||||
|
strict_slashes: Optional[bool] = None,
|
||||||
|
content_type: Optional[str] = None,
|
||||||
|
apply: bool = True,
|
||||||
|
resource_type: Optional[str] = None,
|
||||||
|
index: Optional[Union[str, Sequence[str]]] = None,
|
||||||
|
directory_view: bool = False,
|
||||||
|
directory_handler: Optional[DirectoryHandler] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Register a root to serve files from. The input can either be a
|
||||||
|
file or a directory. This method will enable an easy and simple way
|
||||||
|
to setup the :class:`Route` necessary to serve the static files.
|
||||||
|
|
||||||
|
:param uri: URL path to be used for serving static content
|
||||||
|
:param file_or_directory: Path for the Static file/directory with
|
||||||
|
static files
|
||||||
|
:param pattern: Regex Pattern identifying the valid static files
|
||||||
|
:param use_modified_since: If true, send file modified time, and return
|
||||||
|
not modified if the browser's matches the server's
|
||||||
|
:param use_content_range: If true, process header for range requests
|
||||||
|
and sends the file part that is requested
|
||||||
|
:param stream_large_files: If true, use the
|
||||||
|
:func:`StreamingHTTPResponse.file_stream` handler rather
|
||||||
|
than the :func:`HTTPResponse.file` handler to send the file.
|
||||||
|
If this is an integer, this represents the threshold size to
|
||||||
|
switch to :func:`StreamingHTTPResponse.file_stream`
|
||||||
|
:param name: user defined name used for url_for
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param content_type: user defined content type for header
|
||||||
|
:param apply: If true, will register the route immediately
|
||||||
|
:param resource_type: Explicitly declare a resource to be a "
|
||||||
|
file" or a "dir"
|
||||||
|
:param index: When exposing against a directory, index is the name that
|
||||||
|
will be served as the default file. When multiple files names are
|
||||||
|
passed, then they will be tried in order.
|
||||||
|
:param directory_view: Whether to fallback to showing the directory
|
||||||
|
viewer when exposing a directory
|
||||||
|
:param directory_handler: An instance of :class:`DirectoryHandler`
|
||||||
|
that can be used for explicitly controlling and subclassing the
|
||||||
|
behavior of the default directory handler
|
||||||
|
:return: routes registered on the router
|
||||||
|
:rtype: List[sanic.router.Route]
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = self._generate_name(name)
|
||||||
|
|
||||||
|
if strict_slashes is None and self.strict_slashes is not None:
|
||||||
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
|
if not isinstance(file_or_directory, (str, bytes, PurePath)):
|
||||||
|
raise ValueError(
|
||||||
|
f"Static route must be a valid path, not {file_or_directory}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(file_or_directory, bytes):
|
||||||
|
deprecation(
|
||||||
|
"Serving a static directory with a bytes string is "
|
||||||
|
"deprecated and will be removed in v22.9.",
|
||||||
|
22.9,
|
||||||
|
)
|
||||||
|
file_or_directory = cast(str, file_or_directory.decode())
|
||||||
|
file_or_directory = Path(file_or_directory)
|
||||||
|
|
||||||
|
if directory_handler and (directory_view or index):
|
||||||
|
raise ValueError(
|
||||||
|
"When explicitly setting directory_handler, you cannot "
|
||||||
|
"set either directory_view or index. Instead, pass "
|
||||||
|
"these arguments to your DirectoryHandler instance."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not directory_handler:
|
||||||
|
directory_handler = DirectoryHandler(
|
||||||
|
uri=uri,
|
||||||
|
directory=file_or_directory,
|
||||||
|
directory_view=directory_view,
|
||||||
|
index=index,
|
||||||
|
)
|
||||||
|
|
||||||
|
static = FutureStatic(
|
||||||
|
uri,
|
||||||
|
file_or_directory,
|
||||||
|
pattern,
|
||||||
|
use_modified_since,
|
||||||
|
use_content_range,
|
||||||
|
stream_large_files,
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
strict_slashes,
|
||||||
|
content_type,
|
||||||
|
resource_type,
|
||||||
|
directory_handler,
|
||||||
|
)
|
||||||
|
self._future_statics.add(static)
|
||||||
|
|
||||||
|
if apply:
|
||||||
|
self._apply_static(static)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticHandleMixin(metaclass=SanicMeta):
|
||||||
|
def _apply_static(self, static: FutureStatic) -> Route:
|
||||||
|
return self._register_static(static)
|
||||||
|
|
||||||
|
def _register_static(
|
||||||
|
self,
|
||||||
|
static: FutureStatic,
|
||||||
|
):
|
||||||
|
# TODO: Though sanic is not a file server, I feel like we should
|
||||||
|
# at least make a good effort here. Modified-since is nice, but
|
||||||
|
# we could also look into etags, expires, and caching
|
||||||
|
"""
|
||||||
|
Register a static directory handler with Sanic by adding a route to the
|
||||||
|
router and registering a handler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(static.file_or_directory, bytes):
|
||||||
|
file_or_directory = static.file_or_directory.decode("utf-8")
|
||||||
|
elif isinstance(static.file_or_directory, PurePath):
|
||||||
|
file_or_directory = str(static.file_or_directory)
|
||||||
|
elif not isinstance(static.file_or_directory, str):
|
||||||
|
raise ValueError("Invalid file path string.")
|
||||||
|
else:
|
||||||
|
file_or_directory = static.file_or_directory
|
||||||
|
|
||||||
|
uri = static.uri
|
||||||
|
name = static.name
|
||||||
|
# If we're not trying to match a file directly,
|
||||||
|
# serve from the folder
|
||||||
|
if not static.resource_type:
|
||||||
|
if not path.isfile(file_or_directory):
|
||||||
|
uri = uri.rstrip("/")
|
||||||
|
uri += "/<__file_uri__:path>"
|
||||||
|
elif static.resource_type == "dir":
|
||||||
|
if path.isfile(file_or_directory):
|
||||||
|
raise TypeError(
|
||||||
|
"Resource type improperly identified as directory. "
|
||||||
|
f"'{file_or_directory}'"
|
||||||
|
)
|
||||||
|
uri = uri.rstrip("/")
|
||||||
|
uri += "/<__file_uri__:path>"
|
||||||
|
elif static.resource_type == "file" and not path.isfile(
|
||||||
|
file_or_directory
|
||||||
|
):
|
||||||
|
raise TypeError(
|
||||||
|
"Resource type improperly identified as file. "
|
||||||
|
f"'{file_or_directory}'"
|
||||||
|
)
|
||||||
|
elif static.resource_type != "file":
|
||||||
|
raise ValueError(
|
||||||
|
"The resource_type should be set to 'file' or 'dir'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# special prefix for static files
|
||||||
|
# if not static.name.startswith("_static_"):
|
||||||
|
# name = f"_static_{static.name}"
|
||||||
|
|
||||||
|
_handler = wraps(self._static_request_handler)(
|
||||||
|
partial(
|
||||||
|
self._static_request_handler,
|
||||||
|
file_or_directory=file_or_directory,
|
||||||
|
use_modified_since=static.use_modified_since,
|
||||||
|
use_content_range=static.use_content_range,
|
||||||
|
stream_large_files=static.stream_large_files,
|
||||||
|
content_type=static.content_type,
|
||||||
|
directory_handler=static.directory_handler,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
route, _ = self.route( # type: ignore
|
||||||
|
uri=uri,
|
||||||
|
methods=["GET", "HEAD"],
|
||||||
|
name=name,
|
||||||
|
host=static.host,
|
||||||
|
strict_slashes=static.strict_slashes,
|
||||||
|
static=True,
|
||||||
|
)(_handler)
|
||||||
|
|
||||||
|
return route
|
||||||
|
|
||||||
|
async def _static_request_handler(
|
||||||
|
self,
|
||||||
|
request: Request,
|
||||||
|
*,
|
||||||
|
file_or_directory: PathLike,
|
||||||
|
use_modified_since: bool,
|
||||||
|
use_content_range: bool,
|
||||||
|
stream_large_files: Union[bool, int],
|
||||||
|
directory_handler: DirectoryHandler,
|
||||||
|
content_type: Optional[str] = None,
|
||||||
|
__file_uri__: Optional[str] = None,
|
||||||
|
):
|
||||||
|
not_found = FileNotFound(
|
||||||
|
"File not found",
|
||||||
|
path=file_or_directory,
|
||||||
|
relative_url=__file_uri__,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge served directory and requested file if provided
|
||||||
|
file_path = await self._get_file_path(
|
||||||
|
file_or_directory, __file_uri__, not_found
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = {}
|
||||||
|
# Check if the client has been sent this file before
|
||||||
|
# and it has not been modified since
|
||||||
|
stats = None
|
||||||
|
if use_modified_since:
|
||||||
|
stats = await stat_async(file_path)
|
||||||
|
modified_since = stats.st_mtime
|
||||||
|
response = await validate_file(request.headers, modified_since)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
headers["Last-Modified"] = formatdate(
|
||||||
|
modified_since, usegmt=True
|
||||||
|
)
|
||||||
|
_range = None
|
||||||
|
if use_content_range:
|
||||||
|
_range = None
|
||||||
|
if not stats:
|
||||||
|
stats = await stat_async(file_path)
|
||||||
|
headers["Accept-Ranges"] = "bytes"
|
||||||
|
headers["Content-Length"] = str(stats.st_size)
|
||||||
|
if request.method != "HEAD":
|
||||||
|
try:
|
||||||
|
_range = ContentRangeHandler(request, stats)
|
||||||
|
except HeaderNotFound:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
del headers["Content-Length"]
|
||||||
|
headers.update(_range.headers)
|
||||||
|
|
||||||
|
if "content-type" not in headers:
|
||||||
|
content_type = (
|
||||||
|
content_type
|
||||||
|
or guess_type(file_path)[0]
|
||||||
|
or DEFAULT_HTTP_CONTENT_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
if "charset=" not in content_type and (
|
||||||
|
content_type.startswith("text/")
|
||||||
|
or content_type == "application/javascript"
|
||||||
|
):
|
||||||
|
content_type += "; charset=utf-8"
|
||||||
|
|
||||||
|
headers["Content-Type"] = content_type
|
||||||
|
|
||||||
|
if request.method == "HEAD":
|
||||||
|
return HTTPResponse(headers=headers)
|
||||||
|
else:
|
||||||
|
if stream_large_files:
|
||||||
|
if isinstance(stream_large_files, bool):
|
||||||
|
threshold = 1024 * 1024
|
||||||
|
else:
|
||||||
|
threshold = stream_large_files
|
||||||
|
|
||||||
|
if not stats:
|
||||||
|
stats = await stat_async(file_path)
|
||||||
|
if stats.st_size >= threshold:
|
||||||
|
return await file_stream(
|
||||||
|
file_path, headers=headers, _range=_range
|
||||||
|
)
|
||||||
|
return await file(file_path, headers=headers, _range=_range)
|
||||||
|
except (IsADirectoryError, PermissionError):
|
||||||
|
return await directory_handler.handle(request, request.path)
|
||||||
|
except RangeNotSatisfiable:
|
||||||
|
raise
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise not_found
|
||||||
|
except Exception:
|
||||||
|
error_logger.exception(
|
||||||
|
"Exception in static request handler: "
|
||||||
|
f"path={file_or_directory}, "
|
||||||
|
f"relative_url={__file_uri__}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _get_file_path(self, file_or_directory, __file_uri__, not_found):
|
||||||
|
file_path_raw = Path(unquote(file_or_directory))
|
||||||
|
root_path = file_path = file_path_raw.resolve()
|
||||||
|
|
||||||
|
if __file_uri__:
|
||||||
|
# Strip all / that in the beginning of the URL to help prevent
|
||||||
|
# python from herping a derp and treating the uri as an
|
||||||
|
# absolute path
|
||||||
|
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
|
||||||
|
file_path_raw = Path(file_or_directory, unquoted_file_uri)
|
||||||
|
file_path = file_path_raw.resolve()
|
||||||
|
if (
|
||||||
|
file_path < root_path and not file_path_raw.is_symlink()
|
||||||
|
) or ".." in file_path_raw.parts:
|
||||||
|
error_logger.exception(
|
||||||
|
f"File not found: path={file_or_directory}, "
|
||||||
|
f"relative_url={__file_uri__}"
|
||||||
|
)
|
||||||
|
raise not_found
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_path.relative_to(root_path)
|
||||||
|
except ValueError:
|
||||||
|
if not file_path_raw.is_symlink():
|
||||||
|
error_logger.exception(
|
||||||
|
f"File not found: path={file_or_directory}, "
|
||||||
|
f"relative_url={__file_uri__}"
|
||||||
|
)
|
||||||
|
raise not_found
|
||||||
|
return file_path
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from pathlib import PurePath
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, List, NamedTuple, Optional, Union
|
from typing import Dict, Iterable, List, NamedTuple, Optional, Union
|
||||||
|
|
||||||
|
from sanic.handlers.directory import DirectoryHandler
|
||||||
from sanic.models.handler_types import (
|
from sanic.models.handler_types import (
|
||||||
ErrorMiddlewareType,
|
ErrorMiddlewareType,
|
||||||
ListenerType,
|
ListenerType,
|
||||||
@@ -46,16 +47,17 @@ class FutureException(NamedTuple):
|
|||||||
|
|
||||||
class FutureStatic(NamedTuple):
|
class FutureStatic(NamedTuple):
|
||||||
uri: str
|
uri: str
|
||||||
file_or_directory: Union[str, bytes, PurePath]
|
file_or_directory: Path
|
||||||
pattern: str
|
pattern: str
|
||||||
use_modified_since: bool
|
use_modified_since: bool
|
||||||
use_content_range: bool
|
use_content_range: bool
|
||||||
stream_large_files: bool
|
stream_large_files: Union[bool, int]
|
||||||
name: str
|
name: str
|
||||||
host: Optional[str]
|
host: Optional[str]
|
||||||
strict_slashes: Optional[bool]
|
strict_slashes: Optional[bool]
|
||||||
content_type: Optional[bool]
|
content_type: Optional[str]
|
||||||
resource_type: Optional[str]
|
resource_type: Optional[str]
|
||||||
|
directory_handler: DirectoryHandler
|
||||||
|
|
||||||
|
|
||||||
class FutureSignal(NamedTuple):
|
class FutureSignal(NamedTuple):
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class ConnInfo:
|
|||||||
"client",
|
"client",
|
||||||
"client_ip",
|
"client_ip",
|
||||||
"ctx",
|
"ctx",
|
||||||
|
"lost",
|
||||||
"peername",
|
"peername",
|
||||||
"server_port",
|
"server_port",
|
||||||
"server",
|
"server",
|
||||||
@@ -33,6 +34,7 @@ class ConnInfo:
|
|||||||
|
|
||||||
def __init__(self, transport: TransportProtocol, unix=None):
|
def __init__(self, transport: TransportProtocol, unix=None):
|
||||||
self.ctx = SimpleNamespace()
|
self.ctx = SimpleNamespace()
|
||||||
|
self.lost = False
|
||||||
self.peername = None
|
self.peername = None
|
||||||
self.server = self.client = ""
|
self.server = self.client = ""
|
||||||
self.server_port = self.client_port = 0
|
self.server_port = self.client_port = 0
|
||||||
|
|||||||
0
sanic/pages/__init__.py
Normal file
0
sanic/pages/__init__.py
Normal file
52
sanic/pages/base.py
Normal file
52
sanic/pages/base.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from html5tagger import HTML, Document
|
||||||
|
|
||||||
|
from sanic import __version__ as VERSION
|
||||||
|
from sanic.application.logo import SVG_LOGO_SIMPLE
|
||||||
|
from sanic.pages.css import CSS
|
||||||
|
|
||||||
|
|
||||||
|
class BasePage(ABC, metaclass=CSS): # no cov
|
||||||
|
TITLE = "Sanic"
|
||||||
|
CSS: str
|
||||||
|
|
||||||
|
def __init__(self, debug: bool = True) -> None:
|
||||||
|
self.doc = None
|
||||||
|
self.debug = debug
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style(self) -> str:
|
||||||
|
return self.CSS
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
self.doc = Document(self.TITLE, lang="en")
|
||||||
|
self._head()
|
||||||
|
self._body()
|
||||||
|
self._foot()
|
||||||
|
return str(self.doc)
|
||||||
|
|
||||||
|
def _head(self) -> None:
|
||||||
|
self.doc.style(HTML(self.style))
|
||||||
|
with self.doc.header:
|
||||||
|
self.doc.div(self.TITLE)
|
||||||
|
|
||||||
|
def _foot(self) -> None:
|
||||||
|
with self.doc.footer:
|
||||||
|
self.doc.div("powered by")
|
||||||
|
with self.doc.div:
|
||||||
|
self._sanic_logo()
|
||||||
|
if self.debug:
|
||||||
|
self.doc.div(f"Version {VERSION}")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _body(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
def _sanic_logo(self) -> None:
|
||||||
|
self.doc.a(
|
||||||
|
HTML(SVG_LOGO_SIMPLE),
|
||||||
|
href="https://sanic.dev",
|
||||||
|
target="_blank",
|
||||||
|
referrerpolicy="no-referrer",
|
||||||
|
)
|
||||||
35
sanic/pages/css.py
Normal file
35
sanic/pages/css.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from abc import ABCMeta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
CURRENT_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_style(maybe_style: Optional[str], name: str) -> str:
|
||||||
|
if maybe_style is not None:
|
||||||
|
maybe_path = Path(maybe_style)
|
||||||
|
if maybe_path.exists():
|
||||||
|
return maybe_path.read_text(encoding="UTF-8")
|
||||||
|
return maybe_style
|
||||||
|
maybe_path = CURRENT_DIR / "styles" / f"{name}.css"
|
||||||
|
if maybe_path.exists():
|
||||||
|
return maybe_path.read_text(encoding="UTF-8")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class CSS(ABCMeta):
|
||||||
|
"""Cascade stylesheets, i.e. combine all ancestor styles"""
|
||||||
|
|
||||||
|
def __new__(cls, name, bases, attrs):
|
||||||
|
Page = super().__new__(cls, name, bases, attrs)
|
||||||
|
# Use a locally defined STYLE or the one from styles directory
|
||||||
|
Page.STYLE = _extract_style(attrs.get("STYLE_FILE"), name)
|
||||||
|
Page.STYLE += attrs.get("STYLE_APPEND", "")
|
||||||
|
# Combine with all ancestor styles
|
||||||
|
Page.CSS = "".join(
|
||||||
|
Class.STYLE
|
||||||
|
for Class in reversed(Page.__mro__)
|
||||||
|
if type(Class) is CSS
|
||||||
|
)
|
||||||
|
return Page
|
||||||
66
sanic/pages/directory_page.py
Normal file
66
sanic/pages/directory_page.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from typing import Dict, Iterable
|
||||||
|
|
||||||
|
from html5tagger import E
|
||||||
|
|
||||||
|
from .base import BasePage
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 8): # no cov
|
||||||
|
FileInfo = Dict
|
||||||
|
|
||||||
|
else:
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class FileInfo(TypedDict):
|
||||||
|
icon: str
|
||||||
|
file_name: str
|
||||||
|
file_access: str
|
||||||
|
file_size: str
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryPage(BasePage): # no cov
|
||||||
|
TITLE = "Directory Viewer"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, files: Iterable[FileInfo], url: str, debug: bool
|
||||||
|
) -> None:
|
||||||
|
super().__init__(debug)
|
||||||
|
self.files = files
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def _body(self) -> None:
|
||||||
|
with self.doc.main:
|
||||||
|
self._headline()
|
||||||
|
files = list(self.files)
|
||||||
|
if files:
|
||||||
|
self._file_table(files)
|
||||||
|
else:
|
||||||
|
self.doc.p("The folder is empty.")
|
||||||
|
|
||||||
|
def _headline(self):
|
||||||
|
"""Implement a heading with the current path, combined with
|
||||||
|
breadcrumb links"""
|
||||||
|
with self.doc.h1(id="breadcrumbs"):
|
||||||
|
p = self.url.split("/")[:-1]
|
||||||
|
|
||||||
|
for i, part in enumerate(p):
|
||||||
|
path = "/".join(p[: i + 1]) + "/"
|
||||||
|
with self.doc.a(href=path):
|
||||||
|
self.doc.span(part, class_="dir").span("/", class_="sep")
|
||||||
|
|
||||||
|
def _file_table(self, files: Iterable[FileInfo]):
|
||||||
|
with self.doc.table(class_="autoindex container"):
|
||||||
|
for f in files:
|
||||||
|
self._file_row(**f)
|
||||||
|
|
||||||
|
def _file_row(
|
||||||
|
self,
|
||||||
|
icon: str,
|
||||||
|
file_name: str,
|
||||||
|
file_access: str,
|
||||||
|
file_size: str,
|
||||||
|
):
|
||||||
|
first = E.span(icon, class_="icon").a(file_name, href=file_name)
|
||||||
|
self.doc.tr.td(first).td(file_size).td(file_access)
|
||||||
105
sanic/pages/error.py
Normal file
105
sanic/pages/error.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import tracerite.html
|
||||||
|
|
||||||
|
from html5tagger import E
|
||||||
|
from tracerite import html_traceback, inspector
|
||||||
|
|
||||||
|
from sanic.request import Request
|
||||||
|
|
||||||
|
from .base import BasePage
|
||||||
|
|
||||||
|
|
||||||
|
# Avoid showing the request in the traceback variable inspectors
|
||||||
|
inspector.blacklist_types += (Request,)
|
||||||
|
|
||||||
|
ENDUSER_TEXT = """We're sorry, but it looks like something went wrong. Please try refreshing the page or navigating back to the homepage. If the issue persists, our technical team is working to resolve it as soon as possible. We apologize for the inconvenience and appreciate your patience.""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorPage(BasePage):
|
||||||
|
STYLE_APPEND = tracerite.html.style
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
text: str,
|
||||||
|
request: Request,
|
||||||
|
exc: Exception,
|
||||||
|
full: bool,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
# Internal server errors come with the text of the exception,
|
||||||
|
# which we don't want to show to the user.
|
||||||
|
# FIXME: Needs to be done in a better way, elsewhere
|
||||||
|
if "Internal Server Error" in title:
|
||||||
|
text = "The application encountered an unexpected error and could not continue." # noqa: E501
|
||||||
|
name = request.app.name.replace("_", " ").strip()
|
||||||
|
if name.islower():
|
||||||
|
name = name.title()
|
||||||
|
self.TITLE = E("Application ").strong(name)(
|
||||||
|
" cannot handle your request"
|
||||||
|
)
|
||||||
|
self.title = title
|
||||||
|
self.text = text
|
||||||
|
self.request = request
|
||||||
|
self.exc = exc
|
||||||
|
self.full = full
|
||||||
|
|
||||||
|
def _head(self) -> None:
|
||||||
|
self.doc._script(tracerite.html.javascript)
|
||||||
|
super()._head()
|
||||||
|
|
||||||
|
def _body(self) -> None:
|
||||||
|
debug = self.request.app.debug
|
||||||
|
try:
|
||||||
|
route_name = self.request.route.name
|
||||||
|
except AttributeError:
|
||||||
|
route_name = "[route not found]"
|
||||||
|
with self.doc.main:
|
||||||
|
self.doc.h1(f"⚠️ {self.title}").p(self.text)
|
||||||
|
# Show context details if available on the exception
|
||||||
|
context = getattr(self.exc, "context", None)
|
||||||
|
if context:
|
||||||
|
self._key_value_table(
|
||||||
|
"Issue context", "exception-context", context
|
||||||
|
)
|
||||||
|
|
||||||
|
if not debug:
|
||||||
|
with self.doc.div(id="enduser"):
|
||||||
|
self.doc.p(ENDUSER_TEXT).p.a("Front Page", href="/")
|
||||||
|
return
|
||||||
|
# Show additional details in debug mode,
|
||||||
|
# open by default for 500 errors
|
||||||
|
with self.doc.details(open=self.full, class_="smalltext"):
|
||||||
|
# Show extra details if available on the exception
|
||||||
|
extra = getattr(self.exc, "extra", None)
|
||||||
|
if extra:
|
||||||
|
self._key_value_table(
|
||||||
|
"Issue extra data", "exception-extra", extra
|
||||||
|
)
|
||||||
|
|
||||||
|
self.doc.summary(
|
||||||
|
"Details for developers (Sanic debug mode only)"
|
||||||
|
)
|
||||||
|
if self.exc:
|
||||||
|
self.doc.h2(f"Exception in {route_name}:")
|
||||||
|
self.doc(html_traceback(self.exc, include_js_css=False))
|
||||||
|
|
||||||
|
self._key_value_table(
|
||||||
|
f"{self.request.method} {self.request.path}",
|
||||||
|
"request-headers",
|
||||||
|
self.request.headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _key_value_table(
|
||||||
|
self, title: str, table_id: str, data: Mapping[str, Any]
|
||||||
|
) -> None:
|
||||||
|
self.doc.h2(title)
|
||||||
|
with self.doc.dl(id=table_id, class_="key-value-table smalltext"):
|
||||||
|
for key, value in data.items():
|
||||||
|
# Reading values may cause a new exception, so suppress it
|
||||||
|
try:
|
||||||
|
value = str(value)
|
||||||
|
except Exception:
|
||||||
|
value = E.em("Unable to display value")
|
||||||
|
self.doc.dt.span(key, class_="nobr key").span(": ").dd(value)
|
||||||
128
sanic/pages/styles/BasePage.css
Normal file
128
sanic/pages/styles/BasePage.css
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/** BasePage **/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sanic: #ff0d68;
|
||||||
|
--sanic-blue: #0092FF;
|
||||||
|
--sanic-yellow: #FFE900;
|
||||||
|
--sanic-purple: #833FE3;
|
||||||
|
--sanic-green: #37ae6f;
|
||||||
|
--sanic-background: #f1f5f9;
|
||||||
|
--sanic-text: #1f2937;
|
||||||
|
--sanic-tab-background: #fff;
|
||||||
|
--sanic-tab-text: #0f172a;
|
||||||
|
--sanic-tab-shadow: #adadad;
|
||||||
|
--sanic-highlight-background: var(--sanic-yellow);
|
||||||
|
--sanic-highlight-text: var(--sanic-text);
|
||||||
|
--sanic-header-background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--sanic-purple: #D246DE;
|
||||||
|
--sanic-green: #16DB93;
|
||||||
|
--sanic-background: #111;
|
||||||
|
--sanic-text: #e7e7e7;
|
||||||
|
--sanic-tab-background: #484848;
|
||||||
|
--sanic-tab-text: #e1e1e1;
|
||||||
|
--sanic-tab-shadow: #000;
|
||||||
|
--sanic-highlight-background: var(--sanic-yellow);
|
||||||
|
--sanic-highlight-text: #000;
|
||||||
|
--sanic-header-background: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font: 16px sans-serif;
|
||||||
|
background: var(--sanic-background);
|
||||||
|
color: var(--sanic-text);
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
overflow: hidden auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 125%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body>* {
|
||||||
|
padding: 1rem 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
body>* {
|
||||||
|
padding: 0.5rem 1.5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* Scale everything by rem of 6px-16px by viewport width */
|
||||||
|
font-size: calc(6px + 10 * 100vw / 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 70vh;
|
||||||
|
/* Make sure the footer is closer to bottom */
|
||||||
|
padding: 1rem 2.5rem;
|
||||||
|
/* Generous padding for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.smalltext {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
min-width: 600px;
|
||||||
|
max-width: 1600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: #111;
|
||||||
|
color: #e1e1e1;
|
||||||
|
border-bottom: 1px solid #272727;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #88f;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
span.icon {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo-simple {
|
||||||
|
height: 1.75rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#logo-simple path:last-child {
|
||||||
|
fill: #e1e1e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
sanic/pages/styles/DirectoryPage.css
Normal file
63
sanic/pages/styles/DirectoryPage.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/** DirectoryPage **/
|
||||||
|
#breadcrumbs>a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#breadcrumbs>a .dir {
|
||||||
|
padding: 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#breadcrumbs>a:first-child:hover::before,
|
||||||
|
#breadcrumbs>a .dir:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#breadcrumbs>a:first-child::before {
|
||||||
|
content: "🏠";
|
||||||
|
}
|
||||||
|
|
||||||
|
#breadcrumbs>a:last-child {
|
||||||
|
color: #ff0d68;
|
||||||
|
}
|
||||||
|
|
||||||
|
main a {
|
||||||
|
color: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.autoindex {
|
||||||
|
width: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.autoindex tr {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.autoindex tr:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.autoindex td {
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.autoindex td:first-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.autoindex td:nth-child(2) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.autoindex td:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
table.autoindex tr:hover {
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
}
|
||||||
105
sanic/pages/styles/ErrorPage.css
Normal file
105
sanic/pages/styles/ErrorPage.css
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/** ErrorPage **/
|
||||||
|
#enduser {
|
||||||
|
max-width: 30em;
|
||||||
|
margin: 5em auto 5em auto;
|
||||||
|
text-align: justify;
|
||||||
|
/*text-justify: both;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#enduser a {
|
||||||
|
color: var(--sanic-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#enduser p:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
margin-top: 3em;
|
||||||
|
color: var(--sanic-blue);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite {
|
||||||
|
--tracerite-var: var(--sanic-blue);
|
||||||
|
--tracerite-val: var(--sanic-text);
|
||||||
|
--tracerite-type: var(--sanic-green);
|
||||||
|
--tracerite-exception: var(--sanic);
|
||||||
|
--tracerite-highlight: var(--sanic-yellow);
|
||||||
|
--tracerite-tab: var(--sanic-tab-background);
|
||||||
|
--tracerite-tab-text: var(--sanic-tab-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite>h3 {
|
||||||
|
margin: 0.5rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite .traceback-labels button {
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
line-height: 120% !important;
|
||||||
|
background: var(--tracerite-tab) !important;
|
||||||
|
color: var(--tracerite-tab-text) !important;
|
||||||
|
transition: 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite .traceback-labels {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite .traceback-labels button:hover {
|
||||||
|
filter: contrast(150%) brightness(120%) drop-shadow(0 -0 2px var(--sanic-tab-shadow));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite .traceback-details mark span {
|
||||||
|
background: var(--sanic-highlight-background) !important;
|
||||||
|
color: var(--sanic-highlight-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: var(--sanic-header-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
/*margin-left: -1.5rem; !* Emoji partially in the left margin *!*/
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 1em 0 0.2em 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--sanic-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 5fr;
|
||||||
|
grid-gap: .3em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table dt {
|
||||||
|
color: #888;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.key-value-table dd {
|
||||||
|
word-break: break-all;
|
||||||
|
/* Better breaking for cookies header and such */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracerite .codeline {
|
||||||
|
font-family:
|
||||||
|
"Fira Code",
|
||||||
|
"Source Code Pro",
|
||||||
|
Menlo,
|
||||||
|
Monaco,
|
||||||
|
Consolas,
|
||||||
|
Lucida Console,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ if TYPE_CHECKING:
|
|||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
|
||||||
import email.utils
|
import email.utils
|
||||||
|
import unicodedata
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -38,7 +39,12 @@ from httptools import parse_url
|
|||||||
from httptools.parser.errors import HttpParserInvalidURLError
|
from httptools.parser.errors import HttpParserInvalidURLError
|
||||||
|
|
||||||
from sanic.compat import CancelledErrors, Header
|
from sanic.compat import CancelledErrors, Header
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.constants import (
|
||||||
|
CACHEABLE_HTTP_METHODS,
|
||||||
|
DEFAULT_HTTP_CONTENT_TYPE,
|
||||||
|
IDEMPOTENT_HTTP_METHODS,
|
||||||
|
SAFE_HTTP_METHODS,
|
||||||
|
)
|
||||||
from sanic.exceptions import BadRequest, BadURL, ServerError
|
from sanic.exceptions import BadRequest, BadURL, ServerError
|
||||||
from sanic.headers import (
|
from sanic.headers import (
|
||||||
AcceptContainer,
|
AcceptContainer,
|
||||||
@@ -51,7 +57,7 @@ from sanic.headers import (
|
|||||||
parse_xforwarded,
|
parse_xforwarded,
|
||||||
)
|
)
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import deprecation, error_logger, logger
|
||||||
from sanic.models.protocol_types import TransportProtocol
|
from sanic.models.protocol_types import TransportProtocol
|
||||||
from sanic.response import BaseHTTPResponse, HTTPResponse
|
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||||
|
|
||||||
@@ -98,6 +104,8 @@ class Request:
|
|||||||
"_port",
|
"_port",
|
||||||
"_protocol",
|
"_protocol",
|
||||||
"_remote_addr",
|
"_remote_addr",
|
||||||
|
"_request_middleware_started",
|
||||||
|
"_response_middleware_started",
|
||||||
"_scheme",
|
"_scheme",
|
||||||
"_socket",
|
"_socket",
|
||||||
"_stream_id",
|
"_stream_id",
|
||||||
@@ -121,7 +129,6 @@ class Request:
|
|||||||
"parsed_token",
|
"parsed_token",
|
||||||
"raw_url",
|
"raw_url",
|
||||||
"responded",
|
"responded",
|
||||||
"request_middleware_started",
|
|
||||||
"route",
|
"route",
|
||||||
"stream",
|
"stream",
|
||||||
"transport",
|
"transport",
|
||||||
@@ -139,7 +146,6 @@ class Request:
|
|||||||
head: bytes = b"",
|
head: bytes = b"",
|
||||||
stream_id: int = 0,
|
stream_id: int = 0,
|
||||||
):
|
):
|
||||||
|
|
||||||
self.raw_url = url_bytes
|
self.raw_url = url_bytes
|
||||||
try:
|
try:
|
||||||
self._parsed_url = parse_url(url_bytes)
|
self._parsed_url = parse_url(url_bytes)
|
||||||
@@ -173,7 +179,8 @@ class Request:
|
|||||||
self.parsed_not_grouped_args: DefaultDict[
|
self.parsed_not_grouped_args: DefaultDict[
|
||||||
Tuple[bool, bool, str, str], List[Tuple[str, str]]
|
Tuple[bool, bool, str, str], List[Tuple[str, str]]
|
||||||
] = defaultdict(list)
|
] = defaultdict(list)
|
||||||
self.request_middleware_started = False
|
self._request_middleware_started = False
|
||||||
|
self._response_middleware_started = False
|
||||||
self.responded: bool = False
|
self.responded: bool = False
|
||||||
self.route: Optional[Route] = None
|
self.route: Optional[Route] = None
|
||||||
self.stream: Optional[Stream] = None
|
self.stream: Optional[Stream] = None
|
||||||
@@ -188,7 +195,7 @@ class Request:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_current(cls) -> Request:
|
def get_current(cls) -> Request:
|
||||||
"""
|
"""
|
||||||
Retrieve the currrent request object
|
Retrieve the current request object
|
||||||
|
|
||||||
This implements `Context Variables
|
This implements `Context Variables
|
||||||
<https://docs.python.org/3/library/contextvars.html>`_
|
<https://docs.python.org/3/library/contextvars.html>`_
|
||||||
@@ -214,6 +221,16 @@ class Request:
|
|||||||
def generate_id(*_):
|
def generate_id(*_):
|
||||||
return uuid.uuid4()
|
return uuid.uuid4()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_middleware_started(self):
|
||||||
|
deprecation(
|
||||||
|
"Request.request_middleware_started has been deprecated and will"
|
||||||
|
"be removed. You should set a flag on the request context using"
|
||||||
|
"either middleware or signals if you need this feature.",
|
||||||
|
23.3,
|
||||||
|
)
|
||||||
|
return self._request_middleware_started
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stream_id(self):
|
def stream_id(self):
|
||||||
"""
|
"""
|
||||||
@@ -319,9 +336,14 @@ class Request:
|
|||||||
response = await response # type: ignore
|
response = await response # type: ignore
|
||||||
# Run response middleware
|
# Run response middleware
|
||||||
try:
|
try:
|
||||||
response = await self.app._run_response_middleware(
|
middleware = (
|
||||||
self, response, request_name=self.name
|
self.route and self.route.extra.response_middleware
|
||||||
)
|
) or self.app.response_middleware
|
||||||
|
if middleware and not self._response_middleware_started:
|
||||||
|
self._response_middleware_started = True
|
||||||
|
response = await self.app._run_response_middleware(
|
||||||
|
self, response, middleware
|
||||||
|
)
|
||||||
except CancelledErrors:
|
except CancelledErrors:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -975,6 +997,33 @@ class Request:
|
|||||||
|
|
||||||
return self.transport.scope
|
return self.transport.scope
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_safe(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: Whether the HTTP method is safe.
|
||||||
|
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
return self.method in SAFE_HTTP_METHODS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_idempotent(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: Whether the HTTP method is iempotent.
|
||||||
|
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
return self.method in IDEMPOTENT_HTTP_METHODS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_cacheable(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: Whether the HTTP method is cacheable.
|
||||||
|
See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
return self.method in CACHEABLE_HTTP_METHODS
|
||||||
|
|
||||||
|
|
||||||
class File(NamedTuple):
|
class File(NamedTuple):
|
||||||
"""
|
"""
|
||||||
@@ -1035,6 +1084,16 @@ def parse_multipart_form(body, boundary):
|
|||||||
form_parameters["filename*"]
|
form_parameters["filename*"]
|
||||||
)
|
)
|
||||||
file_name = unquote(value, encoding=encoding)
|
file_name = unquote(value, encoding=encoding)
|
||||||
|
|
||||||
|
# Normalize to NFC (Apple MacOS/iOS send NFD)
|
||||||
|
# Notes:
|
||||||
|
# - No effect for Windows, Linux or Android clients which
|
||||||
|
# already send NFC
|
||||||
|
# - Python open() is tricky (creates files in NFC no matter
|
||||||
|
# which form you use)
|
||||||
|
if file_name is not None:
|
||||||
|
file_name = unicodedata.normalize("NFC", file_name)
|
||||||
|
|
||||||
elif form_header_field == "content-type":
|
elif form_header_field == "content-type":
|
||||||
content_type = form_header_value
|
content_type = form_header_value
|
||||||
content_charset = form_parameters.get("charset", "utf-8")
|
content_charset = form_parameters.get("charset", "utf-8")
|
||||||
|
|||||||
36
sanic/response/__init__.py
Normal file
36
sanic/response/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from .convenience import (
|
||||||
|
empty,
|
||||||
|
file,
|
||||||
|
file_stream,
|
||||||
|
html,
|
||||||
|
json,
|
||||||
|
raw,
|
||||||
|
redirect,
|
||||||
|
text,
|
||||||
|
validate_file,
|
||||||
|
)
|
||||||
|
from .types import (
|
||||||
|
BaseHTTPResponse,
|
||||||
|
HTTPResponse,
|
||||||
|
JSONResponse,
|
||||||
|
ResponseStream,
|
||||||
|
json_dumps,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"BaseHTTPResponse",
|
||||||
|
"HTTPResponse",
|
||||||
|
"JSONResponse",
|
||||||
|
"ResponseStream",
|
||||||
|
"empty",
|
||||||
|
"json",
|
||||||
|
"text",
|
||||||
|
"raw",
|
||||||
|
"html",
|
||||||
|
"validate_file",
|
||||||
|
"file",
|
||||||
|
"redirect",
|
||||||
|
"file_stream",
|
||||||
|
"json_dumps",
|
||||||
|
)
|
||||||
@@ -1,216 +1,25 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate, parsedate_to_datetime
|
||||||
from functools import partial
|
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from time import time
|
from time import time
|
||||||
from typing import (
|
from typing import Any, AnyStr, Callable, Dict, Optional, Union
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
AnyStr,
|
|
||||||
Callable,
|
|
||||||
Coroutine,
|
|
||||||
Dict,
|
|
||||||
Iterator,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
TypeVar,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from sanic.compat import Header, open_async, stat_async
|
from sanic.compat import Header, open_async, stat_async
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||||
from sanic.cookies import CookieJar
|
from sanic.helpers import Default, _default
|
||||||
from sanic.exceptions import SanicException, ServerError
|
from sanic.log import logger
|
||||||
from sanic.helpers import (
|
|
||||||
Default,
|
|
||||||
_default,
|
|
||||||
has_message_body,
|
|
||||||
remove_entity_headers,
|
|
||||||
)
|
|
||||||
from sanic.http import Http
|
|
||||||
from sanic.models.protocol_types import HTMLProtocol, Range
|
from sanic.models.protocol_types import HTMLProtocol, Range
|
||||||
|
|
||||||
|
from .types import HTTPResponse, JSONResponse, ResponseStream
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sanic.asgi import ASGIApp
|
|
||||||
from sanic.http.http3 import HTTPReceiver
|
|
||||||
from sanic.request import Request
|
|
||||||
else:
|
|
||||||
Request = TypeVar("Request")
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from ujson import dumps as json_dumps
|
|
||||||
except ImportError:
|
|
||||||
# This is done in order to ensure that the JSON response is
|
|
||||||
# kept consistent across both ujson and inbuilt json usage.
|
|
||||||
from json import dumps
|
|
||||||
|
|
||||||
json_dumps = partial(dumps, separators=(",", ":"))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseHTTPResponse:
|
|
||||||
"""
|
|
||||||
The base class for all HTTP Responses
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"asgi",
|
|
||||||
"body",
|
|
||||||
"content_type",
|
|
||||||
"stream",
|
|
||||||
"status",
|
|
||||||
"headers",
|
|
||||||
"_cookies",
|
|
||||||
)
|
|
||||||
|
|
||||||
_dumps = json_dumps
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.asgi: bool = False
|
|
||||||
self.body: Optional[bytes] = None
|
|
||||||
self.content_type: Optional[str] = None
|
|
||||||
self.stream: Optional[Union[Http, ASGIApp, HTTPReceiver]] = None
|
|
||||||
self.status: int = None
|
|
||||||
self.headers = Header({})
|
|
||||||
self._cookies: Optional[CookieJar] = None
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
class_name = self.__class__.__name__
|
|
||||||
return f"<{class_name}: {self.status} {self.content_type}>"
|
|
||||||
|
|
||||||
def _encode_body(self, data: Optional[AnyStr]):
|
|
||||||
if data is None:
|
|
||||||
return b""
|
|
||||||
return (
|
|
||||||
data.encode() if hasattr(data, "encode") else data # type: ignore
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cookies(self) -> CookieJar:
|
|
||||||
"""
|
|
||||||
The response cookies. Cookies should be set and written as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
response.cookies["test"] = "It worked!"
|
|
||||||
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
|
|
||||||
response.cookies["test"]["httponly"] = True
|
|
||||||
|
|
||||||
`See user guide re: cookies
|
|
||||||
<https://sanicframework.org/guide/basics/cookies.html>`__
|
|
||||||
|
|
||||||
:return: the cookie jar
|
|
||||||
:rtype: CookieJar
|
|
||||||
"""
|
|
||||||
if self._cookies is None:
|
|
||||||
self._cookies = CookieJar(self.headers)
|
|
||||||
return self._cookies
|
|
||||||
|
|
||||||
@property
|
|
||||||
def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
|
|
||||||
"""
|
|
||||||
Obtain a list of header tuples encoded in bytes for sending.
|
|
||||||
|
|
||||||
Add and remove headers based on status and content_type.
|
|
||||||
|
|
||||||
:return: response headers
|
|
||||||
:rtype: Tuple[Tuple[bytes, bytes], ...]
|
|
||||||
"""
|
|
||||||
# TODO: Make a blacklist set of header names and then filter with that
|
|
||||||
if self.status in (304, 412): # Not Modified, Precondition Failed
|
|
||||||
self.headers = remove_entity_headers(self.headers)
|
|
||||||
if has_message_body(self.status):
|
|
||||||
self.headers.setdefault("content-type", self.content_type)
|
|
||||||
# Encode headers into bytes
|
|
||||||
return (
|
|
||||||
(name.encode("ascii"), f"{value}".encode(errors="surrogateescape"))
|
|
||||||
for name, value in self.headers.items()
|
|
||||||
)
|
|
||||||
|
|
||||||
async def send(
|
|
||||||
self,
|
|
||||||
data: Optional[AnyStr] = None,
|
|
||||||
end_stream: Optional[bool] = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Send any pending response headers and the given data as body.
|
|
||||||
|
|
||||||
:param data: str or bytes to be written
|
|
||||||
:param end_stream: whether to close the stream after this block
|
|
||||||
"""
|
|
||||||
if data is None and end_stream is None:
|
|
||||||
end_stream = True
|
|
||||||
if self.stream is None:
|
|
||||||
raise SanicException(
|
|
||||||
"No stream is connected to the response object instance."
|
|
||||||
)
|
|
||||||
if self.stream.send is None:
|
|
||||||
if end_stream and not data:
|
|
||||||
return
|
|
||||||
raise ServerError(
|
|
||||||
"Response stream was ended, no more response data is "
|
|
||||||
"allowed to be sent."
|
|
||||||
)
|
|
||||||
data = (
|
|
||||||
data.encode() # type: ignore
|
|
||||||
if hasattr(data, "encode")
|
|
||||||
else data or b""
|
|
||||||
)
|
|
||||||
await self.stream.send(
|
|
||||||
data, # type: ignore
|
|
||||||
end_stream=end_stream or False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPResponse(BaseHTTPResponse):
|
|
||||||
"""
|
|
||||||
HTTP response to be sent back to the client.
|
|
||||||
|
|
||||||
:param body: the body content to be returned
|
|
||||||
:type body: Optional[bytes]
|
|
||||||
:param status: HTTP response number. **Default=200**
|
|
||||||
:type status: int
|
|
||||||
:param headers: headers to be returned
|
|
||||||
:type headers: Optional;
|
|
||||||
:param content_type: content type to be returned (as a header)
|
|
||||||
:type content_type: Optional[str]
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
body: Optional[AnyStr] = None,
|
|
||||||
status: int = 200,
|
|
||||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
|
||||||
content_type: Optional[str] = None,
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.content_type: Optional[str] = content_type
|
|
||||||
self.body = self._encode_body(body)
|
|
||||||
self.status = status
|
|
||||||
self.headers = Header(headers or {})
|
|
||||||
self._cookies = None
|
|
||||||
|
|
||||||
async def eof(self):
|
|
||||||
await self.send("", True)
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self.send
|
|
||||||
|
|
||||||
async def __aexit__(self, *_):
|
|
||||||
await self.eof()
|
|
||||||
|
|
||||||
|
|
||||||
def empty(
|
def empty(
|
||||||
status=204, headers: Optional[Dict[str, str]] = None
|
status: int = 204, headers: Optional[Dict[str, str]] = None
|
||||||
) -> HTTPResponse:
|
) -> HTTPResponse:
|
||||||
"""
|
"""
|
||||||
Returns an empty response to the client.
|
Returns an empty response to the client.
|
||||||
@@ -227,8 +36,8 @@ def json(
|
|||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
content_type: str = "application/json",
|
content_type: str = "application/json",
|
||||||
dumps: Optional[Callable[..., str]] = None,
|
dumps: Optional[Callable[..., str]] = None,
|
||||||
**kwargs,
|
**kwargs: Any,
|
||||||
) -> HTTPResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Returns response object with body in json format.
|
Returns response object with body in json format.
|
||||||
|
|
||||||
@@ -237,13 +46,14 @@ def json(
|
|||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||||
"""
|
"""
|
||||||
if not dumps:
|
|
||||||
dumps = BaseHTTPResponse._dumps
|
return JSONResponse(
|
||||||
return HTTPResponse(
|
body,
|
||||||
dumps(body, **kwargs),
|
|
||||||
headers=headers,
|
|
||||||
status=status,
|
status=status,
|
||||||
|
headers=headers,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
|
dumps=dumps,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -319,9 +129,34 @@ def html(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_file(
|
||||||
|
request_headers: Header, last_modified: Union[datetime, float, int]
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
if_modified_since = request_headers.getone("If-Modified-Since")
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if_modified_since = parsedate_to_datetime(if_modified_since)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
logger.warning(
|
||||||
|
"Ignorning invalid If-Modified-Since header received: " "'%s'",
|
||||||
|
if_modified_since,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not isinstance(last_modified, datetime):
|
||||||
|
last_modified = datetime.fromtimestamp(
|
||||||
|
float(last_modified), tz=timezone.utc
|
||||||
|
).replace(microsecond=0)
|
||||||
|
if last_modified <= if_modified_since:
|
||||||
|
return HTTPResponse(status=304)
|
||||||
|
|
||||||
|
|
||||||
async def file(
|
async def file(
|
||||||
location: Union[str, PurePath],
|
location: Union[str, PurePath],
|
||||||
status: int = 200,
|
status: int = 200,
|
||||||
|
request_headers: Optional[Header] = None,
|
||||||
|
validate_when_requested: bool = True,
|
||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
filename: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
@@ -331,7 +166,12 @@ async def file(
|
|||||||
_range: Optional[Range] = None,
|
_range: Optional[Range] = None,
|
||||||
) -> HTTPResponse:
|
) -> HTTPResponse:
|
||||||
"""Return a response object with file data.
|
"""Return a response object with file data.
|
||||||
|
:param status: HTTP response code. Won't enforce the passed in
|
||||||
|
status if only a part of the content will be sent (206)
|
||||||
|
or file is being validated (304).
|
||||||
|
:param request_headers: The request headers.
|
||||||
|
:param validate_when_requested: If True, will validate the
|
||||||
|
file when requested.
|
||||||
:param location: Location of file on system.
|
:param location: Location of file on system.
|
||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
@@ -341,11 +181,6 @@ async def file(
|
|||||||
:param no_store: Any cache should not store this response.
|
:param no_store: Any cache should not store this response.
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
headers = headers or {}
|
|
||||||
if filename:
|
|
||||||
headers.setdefault(
|
|
||||||
"Content-Disposition", f'attachment; filename="{filename}"'
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(last_modified, datetime):
|
if isinstance(last_modified, datetime):
|
||||||
last_modified = last_modified.replace(microsecond=0).timestamp()
|
last_modified = last_modified.replace(microsecond=0).timestamp()
|
||||||
@@ -353,9 +188,24 @@ async def file(
|
|||||||
stat = await stat_async(location)
|
stat = await stat_async(location)
|
||||||
last_modified = stat.st_mtime
|
last_modified = stat.st_mtime
|
||||||
|
|
||||||
|
if (
|
||||||
|
validate_when_requested
|
||||||
|
and request_headers is not None
|
||||||
|
and last_modified
|
||||||
|
):
|
||||||
|
response = await validate_file(request_headers, last_modified)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
|
||||||
|
headers = headers or {}
|
||||||
if last_modified:
|
if last_modified:
|
||||||
headers.setdefault(
|
headers.setdefault(
|
||||||
"last-modified", formatdate(last_modified, usegmt=True)
|
"Last-Modified", formatdate(last_modified, usegmt=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
headers.setdefault(
|
||||||
|
"Content-Disposition", f'attachment; filename="{filename}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
if no_store:
|
if no_store:
|
||||||
@@ -424,80 +274,6 @@ def redirect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ResponseStream:
|
|
||||||
"""
|
|
||||||
ResponseStream is a compat layer to bridge the gap after the deprecation
|
|
||||||
of StreamingHTTPResponse. It will be removed when:
|
|
||||||
- file_stream is moved to new style streaming
|
|
||||||
- file and file_stream are combined into a single API
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"_cookies",
|
|
||||||
"content_type",
|
|
||||||
"headers",
|
|
||||||
"request",
|
|
||||||
"response",
|
|
||||||
"status",
|
|
||||||
"streaming_fn",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
streaming_fn: Callable[
|
|
||||||
[Union[BaseHTTPResponse, ResponseStream]],
|
|
||||||
Coroutine[Any, Any, None],
|
|
||||||
],
|
|
||||||
status: int = 200,
|
|
||||||
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
|
||||||
content_type: Optional[str] = None,
|
|
||||||
):
|
|
||||||
self.streaming_fn = streaming_fn
|
|
||||||
self.status = status
|
|
||||||
self.headers = headers or Header()
|
|
||||||
self.content_type = content_type
|
|
||||||
self.request: Optional[Request] = None
|
|
||||||
self._cookies: Optional[CookieJar] = None
|
|
||||||
|
|
||||||
async def write(self, message: str):
|
|
||||||
await self.response.send(message)
|
|
||||||
|
|
||||||
async def stream(self) -> HTTPResponse:
|
|
||||||
if not self.request:
|
|
||||||
raise ServerError("Attempted response to unknown request")
|
|
||||||
self.response = await self.request.respond(
|
|
||||||
headers=self.headers,
|
|
||||||
status=self.status,
|
|
||||||
content_type=self.content_type,
|
|
||||||
)
|
|
||||||
await self.streaming_fn(self)
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
async def eof(self) -> None:
|
|
||||||
await self.response.eof()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cookies(self) -> CookieJar:
|
|
||||||
if self._cookies is None:
|
|
||||||
self._cookies = CookieJar(self.headers)
|
|
||||||
return self._cookies
|
|
||||||
|
|
||||||
@property
|
|
||||||
def processed_headers(self):
|
|
||||||
return self.response.processed_headers
|
|
||||||
|
|
||||||
@property
|
|
||||||
def body(self):
|
|
||||||
return self.response.body
|
|
||||||
|
|
||||||
def __call__(self, request: Request) -> ResponseStream:
|
|
||||||
self.request = request
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __await__(self):
|
|
||||||
return self.stream().__await__()
|
|
||||||
|
|
||||||
|
|
||||||
async def file_stream(
|
async def file_stream(
|
||||||
location: Union[str, PurePath],
|
location: Union[str, PurePath],
|
||||||
status: int = 200,
|
status: int = 200,
|
||||||
453
sanic/response/types.py
Normal file
453
sanic/response/types.py
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
AnyStr,
|
||||||
|
Callable,
|
||||||
|
Coroutine,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
from sanic.compat import Header
|
||||||
|
from sanic.cookies import CookieJar
|
||||||
|
from sanic.exceptions import SanicException, ServerError
|
||||||
|
from sanic.helpers import (
|
||||||
|
Default,
|
||||||
|
_default,
|
||||||
|
has_message_body,
|
||||||
|
remove_entity_headers,
|
||||||
|
)
|
||||||
|
from sanic.http import Http
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sanic.asgi import ASGIApp
|
||||||
|
from sanic.http.http3 import HTTPReceiver
|
||||||
|
from sanic.request import Request
|
||||||
|
else:
|
||||||
|
Request = TypeVar("Request")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ujson import dumps as json_dumps
|
||||||
|
except ImportError:
|
||||||
|
# This is done in order to ensure that the JSON response is
|
||||||
|
# kept consistent across both ujson and inbuilt json usage.
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
json_dumps = partial(dumps, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHTTPResponse:
|
||||||
|
"""
|
||||||
|
The base class for all HTTP Responses
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"asgi",
|
||||||
|
"body",
|
||||||
|
"content_type",
|
||||||
|
"stream",
|
||||||
|
"status",
|
||||||
|
"headers",
|
||||||
|
"_cookies",
|
||||||
|
)
|
||||||
|
|
||||||
|
_dumps = json_dumps
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.asgi: bool = False
|
||||||
|
self.body: Optional[bytes] = None
|
||||||
|
self.content_type: Optional[str] = None
|
||||||
|
self.stream: Optional[Union[Http, ASGIApp, HTTPReceiver]] = None
|
||||||
|
self.status: int = None
|
||||||
|
self.headers = Header({})
|
||||||
|
self._cookies: Optional[CookieJar] = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
class_name = self.__class__.__name__
|
||||||
|
return f"<{class_name}: {self.status} {self.content_type}>"
|
||||||
|
|
||||||
|
def _encode_body(self, data: Optional[AnyStr]):
|
||||||
|
if data is None:
|
||||||
|
return b""
|
||||||
|
return (
|
||||||
|
data.encode() if hasattr(data, "encode") else data # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookies(self) -> CookieJar:
|
||||||
|
"""
|
||||||
|
The response cookies. Cookies should be set and written as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
response.cookies["test"] = "It worked!"
|
||||||
|
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
|
||||||
|
response.cookies["test"]["httponly"] = True
|
||||||
|
|
||||||
|
`See user guide re: cookies
|
||||||
|
<https://sanic.dev/en/guide/basics/cookies.html>`
|
||||||
|
|
||||||
|
:return: the cookie jar
|
||||||
|
:rtype: CookieJar
|
||||||
|
"""
|
||||||
|
if self._cookies is None:
|
||||||
|
self._cookies = CookieJar(self.headers)
|
||||||
|
return self._cookies
|
||||||
|
|
||||||
|
@property
|
||||||
|
def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]:
|
||||||
|
"""
|
||||||
|
Obtain a list of header tuples encoded in bytes for sending.
|
||||||
|
|
||||||
|
Add and remove headers based on status and content_type.
|
||||||
|
|
||||||
|
:return: response headers
|
||||||
|
:rtype: Tuple[Tuple[bytes, bytes], ...]
|
||||||
|
"""
|
||||||
|
# TODO: Make a blacklist set of header names and then filter with that
|
||||||
|
if self.status in (304, 412): # Not Modified, Precondition Failed
|
||||||
|
self.headers = remove_entity_headers(self.headers)
|
||||||
|
if has_message_body(self.status):
|
||||||
|
self.headers.setdefault("content-type", self.content_type)
|
||||||
|
# Encode headers into bytes
|
||||||
|
return (
|
||||||
|
(name.encode("ascii"), f"{value}".encode(errors="surrogateescape"))
|
||||||
|
for name, value in self.headers.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send(
|
||||||
|
self,
|
||||||
|
data: Optional[AnyStr] = None,
|
||||||
|
end_stream: Optional[bool] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Send any pending response headers and the given data as body.
|
||||||
|
|
||||||
|
:param data: str or bytes to be written
|
||||||
|
:param end_stream: whether to close the stream after this block
|
||||||
|
"""
|
||||||
|
if data is None and end_stream is None:
|
||||||
|
end_stream = True
|
||||||
|
if self.stream is None:
|
||||||
|
raise SanicException(
|
||||||
|
"No stream is connected to the response object instance."
|
||||||
|
)
|
||||||
|
if self.stream.send is None:
|
||||||
|
if end_stream and not data:
|
||||||
|
return
|
||||||
|
raise ServerError(
|
||||||
|
"Response stream was ended, no more response data is "
|
||||||
|
"allowed to be sent."
|
||||||
|
)
|
||||||
|
data = (
|
||||||
|
data.encode() # type: ignore
|
||||||
|
if hasattr(data, "encode")
|
||||||
|
else data or b""
|
||||||
|
)
|
||||||
|
await self.stream.send(
|
||||||
|
data, # type: ignore
|
||||||
|
end_stream=end_stream or False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPResponse(BaseHTTPResponse):
|
||||||
|
"""
|
||||||
|
HTTP response to be sent back to the client.
|
||||||
|
|
||||||
|
:param body: the body content to be returned
|
||||||
|
:type body: Optional[bytes]
|
||||||
|
:param status: HTTP response number. **Default=200**
|
||||||
|
:type status: int
|
||||||
|
:param headers: headers to be returned
|
||||||
|
:type headers: Optional;
|
||||||
|
:param content_type: content type to be returned (as a header)
|
||||||
|
:type content_type: Optional[str]
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
body: Optional[Any] = None,
|
||||||
|
status: int = 200,
|
||||||
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
|
content_type: Optional[str] = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.content_type: Optional[str] = content_type
|
||||||
|
self.body = self._encode_body(body)
|
||||||
|
self.status = status
|
||||||
|
self.headers = Header(headers or {})
|
||||||
|
self._cookies = None
|
||||||
|
|
||||||
|
async def eof(self):
|
||||||
|
await self.send("", True)
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self.send
|
||||||
|
|
||||||
|
async def __aexit__(self, *_):
|
||||||
|
await self.eof()
|
||||||
|
|
||||||
|
|
||||||
|
class JSONResponse(HTTPResponse):
|
||||||
|
"""
|
||||||
|
HTTP response to be sent back to the client, when the response
|
||||||
|
is of json type. Offers several utilities to manipulate common
|
||||||
|
json data types.
|
||||||
|
|
||||||
|
:param body: the body content to be returned
|
||||||
|
:type body: Optional[Any]
|
||||||
|
:param status: HTTP response number. **Default=200**
|
||||||
|
:type status: int
|
||||||
|
:param headers: headers to be returned
|
||||||
|
:type headers: Optional
|
||||||
|
:param content_type: content type to be returned (as a header)
|
||||||
|
:type content_type: Optional[str]
|
||||||
|
:param dumps: json.dumps function to use
|
||||||
|
:type dumps: Optional[Callable]
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"_body",
|
||||||
|
"_body_manually_set",
|
||||||
|
"_initialized",
|
||||||
|
"_raw_body",
|
||||||
|
"_use_dumps",
|
||||||
|
"_use_dumps_kwargs",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
body: Optional[Any] = None,
|
||||||
|
status: int = 200,
|
||||||
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
|
content_type: Optional[str] = None,
|
||||||
|
dumps: Optional[Callable[..., str]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
self._initialized = False
|
||||||
|
self._body_manually_set = False
|
||||||
|
|
||||||
|
self._use_dumps = dumps or BaseHTTPResponse._dumps
|
||||||
|
self._use_dumps_kwargs = kwargs
|
||||||
|
|
||||||
|
self._raw_body = body
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
self._encode_body(self._use_dumps(body, **self._use_dumps_kwargs)),
|
||||||
|
headers=headers,
|
||||||
|
status=status,
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
def _check_body_not_manually_set(self):
|
||||||
|
if self._body_manually_set:
|
||||||
|
raise SanicException(
|
||||||
|
"Cannot use raw_body after body has been manually set."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def raw_body(self) -> Optional[Any]:
|
||||||
|
"""Returns the raw body, as long as body has not been manually
|
||||||
|
set previously.
|
||||||
|
|
||||||
|
NOTE: This object should not be mutated, as it will not be
|
||||||
|
reflected in the response body. If you need to mutate the
|
||||||
|
response body, consider using one of the provided methods in
|
||||||
|
this class or alternatively call set_body() with the mutated
|
||||||
|
object afterwards or set the raw_body property to it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_body_not_manually_set()
|
||||||
|
return self._raw_body
|
||||||
|
|
||||||
|
@raw_body.setter
|
||||||
|
def raw_body(self, value: Any):
|
||||||
|
self._body_manually_set = False
|
||||||
|
self._body = self._encode_body(
|
||||||
|
self._use_dumps(value, **self._use_dumps_kwargs)
|
||||||
|
)
|
||||||
|
self._raw_body = value
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
def body(self) -> Optional[bytes]: # type: ignore
|
||||||
|
return self._body
|
||||||
|
|
||||||
|
@body.setter
|
||||||
|
def body(self, value: Optional[bytes]):
|
||||||
|
self._body = value
|
||||||
|
if not self._initialized:
|
||||||
|
return
|
||||||
|
self._body_manually_set = True
|
||||||
|
|
||||||
|
def set_body(
|
||||||
|
self,
|
||||||
|
body: Any,
|
||||||
|
dumps: Optional[Callable[..., str]] = None,
|
||||||
|
**dumps_kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Sets a new response body using the given dumps function
|
||||||
|
and kwargs, or falling back to the defaults given when
|
||||||
|
creating the object if none are specified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._body_manually_set = False
|
||||||
|
self._raw_body = body
|
||||||
|
|
||||||
|
use_dumps = dumps or self._use_dumps
|
||||||
|
use_dumps_kwargs = dumps_kwargs if dumps else self._use_dumps_kwargs
|
||||||
|
|
||||||
|
self._body = self._encode_body(use_dumps(body, **use_dumps_kwargs))
|
||||||
|
|
||||||
|
def append(self, value: Any) -> None:
|
||||||
|
"""Appends a value to the response raw_body, ensuring that
|
||||||
|
body is kept up to date. This can only be used if raw_body
|
||||||
|
is a list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_body_not_manually_set()
|
||||||
|
|
||||||
|
if not isinstance(self._raw_body, list):
|
||||||
|
raise SanicException("Cannot append to a non-list object.")
|
||||||
|
|
||||||
|
self._raw_body.append(value)
|
||||||
|
self.raw_body = self._raw_body
|
||||||
|
|
||||||
|
def extend(self, value: Any) -> None:
|
||||||
|
"""Extends the response's raw_body with the given values, ensuring
|
||||||
|
that body is kept up to date. This can only be used if raw_body is
|
||||||
|
a list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_body_not_manually_set()
|
||||||
|
|
||||||
|
if not isinstance(self._raw_body, list):
|
||||||
|
raise SanicException("Cannot extend a non-list object.")
|
||||||
|
|
||||||
|
self._raw_body.extend(value)
|
||||||
|
self.raw_body = self._raw_body
|
||||||
|
|
||||||
|
def update(self, *args, **kwargs) -> None:
|
||||||
|
"""Updates the response's raw_body with the given values, ensuring
|
||||||
|
that body is kept up to date. This can only be used if raw_body is
|
||||||
|
a dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_body_not_manually_set()
|
||||||
|
|
||||||
|
if not isinstance(self._raw_body, dict):
|
||||||
|
raise SanicException("Cannot update a non-dict object.")
|
||||||
|
|
||||||
|
self._raw_body.update(*args, **kwargs)
|
||||||
|
self.raw_body = self._raw_body
|
||||||
|
|
||||||
|
def pop(self, key: Any, default: Any = _default) -> Any:
|
||||||
|
"""Pops a key from the response's raw_body, ensuring that body is
|
||||||
|
kept up to date. This can only be used if raw_body is a dict or a
|
||||||
|
list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._check_body_not_manually_set()
|
||||||
|
|
||||||
|
if not isinstance(self._raw_body, (list, dict)):
|
||||||
|
raise SanicException(
|
||||||
|
"Cannot pop from a non-list and non-dict object."
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(default, Default):
|
||||||
|
value = self._raw_body.pop(key)
|
||||||
|
elif isinstance(self._raw_body, list):
|
||||||
|
raise TypeError("pop doesn't accept a default argument for lists")
|
||||||
|
else:
|
||||||
|
value = self._raw_body.pop(key, default)
|
||||||
|
|
||||||
|
self.raw_body = self._raw_body
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseStream:
|
||||||
|
"""
|
||||||
|
ResponseStream is a compat layer to bridge the gap after the deprecation
|
||||||
|
of StreamingHTTPResponse. It will be removed when:
|
||||||
|
- file_stream is moved to new style streaming
|
||||||
|
- file and file_stream are combined into a single API
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"_cookies",
|
||||||
|
"content_type",
|
||||||
|
"headers",
|
||||||
|
"request",
|
||||||
|
"response",
|
||||||
|
"status",
|
||||||
|
"streaming_fn",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
streaming_fn: Callable[
|
||||||
|
[Union[BaseHTTPResponse, ResponseStream]],
|
||||||
|
Coroutine[Any, Any, None],
|
||||||
|
],
|
||||||
|
status: int = 200,
|
||||||
|
headers: Optional[Union[Header, Dict[str, str]]] = None,
|
||||||
|
content_type: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self.streaming_fn = streaming_fn
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers or Header()
|
||||||
|
self.content_type = content_type
|
||||||
|
self.request: Optional[Request] = None
|
||||||
|
self._cookies: Optional[CookieJar] = None
|
||||||
|
|
||||||
|
async def write(self, message: str):
|
||||||
|
await self.response.send(message)
|
||||||
|
|
||||||
|
async def stream(self) -> HTTPResponse:
|
||||||
|
if not self.request:
|
||||||
|
raise ServerError("Attempted response to unknown request")
|
||||||
|
self.response = await self.request.respond(
|
||||||
|
headers=self.headers,
|
||||||
|
status=self.status,
|
||||||
|
content_type=self.content_type,
|
||||||
|
)
|
||||||
|
await self.streaming_fn(self)
|
||||||
|
return self.response
|
||||||
|
|
||||||
|
async def eof(self) -> None:
|
||||||
|
await self.response.eof()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookies(self) -> CookieJar:
|
||||||
|
if self._cookies is None:
|
||||||
|
self._cookies = CookieJar(self.headers)
|
||||||
|
return self._cookies
|
||||||
|
|
||||||
|
@property
|
||||||
|
def processed_headers(self):
|
||||||
|
return self.response.processed_headers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def body(self):
|
||||||
|
return self.response.body
|
||||||
|
|
||||||
|
def __call__(self, request: Request) -> ResponseStream:
|
||||||
|
self.request = request
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
return self.stream().__await__()
|
||||||
@@ -39,13 +39,13 @@ class Router(BaseRouter):
|
|||||||
extra={"host": host} if host else None,
|
extra={"host": host} if host else None,
|
||||||
)
|
)
|
||||||
except RoutingNotFound as e:
|
except RoutingNotFound as e:
|
||||||
raise NotFound("Requested URL {} not found".format(e.path))
|
raise NotFound(f"Requested URL {e.path} not found") from None
|
||||||
except NoMethod as e:
|
except NoMethod as e:
|
||||||
raise MethodNotAllowed(
|
raise MethodNotAllowed(
|
||||||
"Method {} not allowed for URL {}".format(method, path),
|
f"Method {method} not allowed for URL {path}",
|
||||||
method=method,
|
method=method,
|
||||||
allowed_methods=e.allowed_methods,
|
allowed_methods=e.allowed_methods,
|
||||||
)
|
) from None
|
||||||
|
|
||||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||||
def get( # type: ignore
|
def get( # type: ignore
|
||||||
@@ -61,6 +61,7 @@ class Router(BaseRouter):
|
|||||||
correct response
|
correct response
|
||||||
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
return self._get(path, method, host)
|
return self._get(path, method, host)
|
||||||
|
|
||||||
def add( # type: ignore
|
def add( # type: ignore
|
||||||
@@ -133,14 +134,14 @@ class Router(BaseRouter):
|
|||||||
params.update({"requirements": {"host": host}})
|
params.update({"requirements": {"host": host}})
|
||||||
|
|
||||||
route = super().add(**params) # type: ignore
|
route = super().add(**params) # type: ignore
|
||||||
route.ctx.ignore_body = ignore_body
|
route.extra.ignore_body = ignore_body
|
||||||
route.ctx.stream = stream
|
route.extra.stream = stream
|
||||||
route.ctx.hosts = hosts
|
route.extra.hosts = hosts
|
||||||
route.ctx.static = static
|
route.extra.static = static
|
||||||
route.ctx.error_format = error_format
|
route.extra.error_format = error_format
|
||||||
|
|
||||||
if error_format:
|
if error_format:
|
||||||
check_error_format(route.ctx.error_format)
|
check_error_format(route.extra.error_format)
|
||||||
|
|
||||||
routes.append(route)
|
routes.append(route)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from time import sleep
|
|||||||
|
|
||||||
def _iter_module_files():
|
def _iter_module_files():
|
||||||
"""This iterates over all relevant Python files.
|
"""This iterates over all relevant Python files.
|
||||||
|
|
||||||
It goes through all
|
It goes through all
|
||||||
loaded files from modules, all files in folders of already loaded modules
|
loaded files from modules, all files in folders of already loaded modules
|
||||||
as well as all files reachable through a package.
|
as well as all files reachable through a package.
|
||||||
@@ -52,7 +51,7 @@ def restart_with_reloader(changed=None):
|
|||||||
this one.
|
this one.
|
||||||
"""
|
"""
|
||||||
reloaded = ",".join(changed) if changed else ""
|
reloaded = ",".join(changed) if changed else ""
|
||||||
return subprocess.Popen(
|
return subprocess.Popen( # nosec B603
|
||||||
_get_args_for_reloading(),
|
_get_args_for_reloading(),
|
||||||
env={
|
env={
|
||||||
**os.environ,
|
**os.environ,
|
||||||
@@ -79,7 +78,6 @@ def _check_file(filename, mtimes):
|
|||||||
|
|
||||||
def watchdog(sleep_interval, reload_dirs):
|
def watchdog(sleep_interval, reload_dirs):
|
||||||
"""Watch project files, restart worker process if a change happened.
|
"""Watch project files, restart worker process if a change happened.
|
||||||
|
|
||||||
:param sleep_interval: interval in second.
|
:param sleep_interval: interval in second.
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
@@ -96,7 +94,6 @@ def watchdog(sleep_interval, reload_dirs):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
changed = set()
|
changed = set()
|
||||||
for filename in itertools.chain(
|
for filename in itertools.chain(
|
||||||
_iter_module_files(),
|
_iter_module_files(),
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
from distutils.util import strtobool
|
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
|
||||||
from sanic.compat import OS_IS_WINDOWS
|
from sanic.compat import OS_IS_WINDOWS
|
||||||
from sanic.log import error_logger
|
from sanic.log import error_logger
|
||||||
|
from sanic.utils import str_to_bool
|
||||||
|
|
||||||
|
|
||||||
def try_use_uvloop() -> None:
|
def try_use_uvloop() -> None:
|
||||||
@@ -34,7 +35,7 @@ def try_use_uvloop() -> None:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no"))
|
uvloop_install_removed = str_to_bool(getenv("SANIC_NO_UVLOOP", "no"))
|
||||||
if uvloop_install_removed:
|
if uvloop_install_removed:
|
||||||
error_logger.info(
|
error_logger.info(
|
||||||
"You are requesting to run Sanic using uvloop, but the "
|
"You are requesting to run Sanic using uvloop, but the "
|
||||||
@@ -47,3 +48,19 @@ def try_use_uvloop() -> None:
|
|||||||
|
|
||||||
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
|
||||||
|
def try_windows_loop():
|
||||||
|
if not OS_IS_WINDOWS:
|
||||||
|
error_logger.warning(
|
||||||
|
"You are trying to use an event loop policy that is not "
|
||||||
|
"compatible with your system. You can simply let Sanic handle "
|
||||||
|
"selecting the best loop for you. Sanic will now continue to run "
|
||||||
|
"using the default event loop."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8) and not isinstance(
|
||||||
|
asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy
|
||||||
|
):
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sanic.exceptions import RequestCancelled
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from asyncio import CancelledError
|
|
||||||
from asyncio.transports import Transport
|
from asyncio.transports import Transport
|
||||||
from time import monotonic as current_time
|
from time import monotonic as current_time
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ class SanicProtocol(asyncio.Protocol):
|
|||||||
"""
|
"""
|
||||||
await self._can_write.wait()
|
await self._can_write.wait()
|
||||||
if self.transport.is_closing():
|
if self.transport.is_closing():
|
||||||
raise CancelledError
|
raise RequestCancelled
|
||||||
self.transport.write(data)
|
self.transport.write(data)
|
||||||
self._time = current_time()
|
self._time = current_time()
|
||||||
|
|
||||||
@@ -120,6 +121,7 @@ class SanicProtocol(asyncio.Protocol):
|
|||||||
try:
|
try:
|
||||||
self.connections.discard(self)
|
self.connections.discard(self)
|
||||||
self.resume_writing()
|
self.resume_writing()
|
||||||
|
self.conn_info.lost = True
|
||||||
if self._task:
|
if self._task:
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import sys
|
|||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
from time import monotonic as current_time
|
from time import monotonic as current_time
|
||||||
|
|
||||||
from sanic.exceptions import RequestTimeout, ServiceUnavailable
|
from sanic.exceptions import (
|
||||||
|
RequestCancelled,
|
||||||
|
RequestTimeout,
|
||||||
|
ServiceUnavailable,
|
||||||
|
)
|
||||||
from sanic.http import Http, Stage
|
from sanic.http import Http, Stage
|
||||||
from sanic.log import Colors, error_logger, logger
|
from sanic.log import Colors, error_logger, logger
|
||||||
from sanic.models.server_types import ConnInfo
|
from sanic.models.server_types import ConnInfo
|
||||||
@@ -225,7 +229,7 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
await self._can_write.wait()
|
await self._can_write.wait()
|
||||||
if self.transport.is_closing():
|
if self.transport.is_closing():
|
||||||
raise CancelledError
|
raise RequestCancelled
|
||||||
await self.app.dispatch(
|
await self.app.dispatch(
|
||||||
"http.lifecycle.send",
|
"http.lifecycle.send",
|
||||||
inline=True,
|
inline=True,
|
||||||
@@ -265,7 +269,6 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
|
|||||||
error_logger.exception("protocol.connect_made")
|
error_logger.exception("protocol.connect_made")
|
||||||
|
|
||||||
def data_received(self, data: bytes):
|
def data_received(self, data: bytes):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._time = current_time()
|
self._time = current_time()
|
||||||
if not data:
|
if not data:
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
from typing import TYPE_CHECKING, Optional, Sequence, cast
|
from typing import TYPE_CHECKING, Optional, Sequence, cast
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN
|
|
||||||
from websockets.server import ServerConnection
|
try: # websockets < 11.0
|
||||||
|
from websockets.connection import State
|
||||||
|
from websockets.server import ServerConnection as ServerProtocol
|
||||||
|
except ImportError: # websockets >= 11.0
|
||||||
|
from websockets.protocol import State # type: ignore
|
||||||
|
from websockets.server import ServerProtocol # type: ignore
|
||||||
|
|
||||||
from websockets.typing import Subprotocol
|
from websockets.typing import Subprotocol
|
||||||
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
@@ -15,6 +21,11 @@ if TYPE_CHECKING:
|
|||||||
from websockets import http11
|
from websockets import http11
|
||||||
|
|
||||||
|
|
||||||
|
OPEN = State.OPEN
|
||||||
|
CLOSING = State.CLOSING
|
||||||
|
CLOSED = State.CLOSED
|
||||||
|
|
||||||
|
|
||||||
class WebSocketProtocol(HttpProtocol):
|
class WebSocketProtocol(HttpProtocol):
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"websocket",
|
"websocket",
|
||||||
@@ -74,7 +85,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
# Called by Sanic Server when shutting down
|
# Called by Sanic Server when shutting down
|
||||||
# If we've upgraded to websocket, shut it down
|
# If we've upgraded to websocket, shut it down
|
||||||
if self.websocket is not None:
|
if self.websocket is not None:
|
||||||
if self.websocket.connection.state in (CLOSING, CLOSED):
|
if self.websocket.ws_proto.state in (CLOSING, CLOSED):
|
||||||
return True
|
return True
|
||||||
elif self.websocket.loop is not None:
|
elif self.websocket.loop is not None:
|
||||||
self.websocket.loop.create_task(self.websocket.close(1001))
|
self.websocket.loop.create_task(self.websocket.close(1001))
|
||||||
@@ -90,7 +101,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
try:
|
try:
|
||||||
if subprotocols is not None:
|
if subprotocols is not None:
|
||||||
# subprotocols can be a set or frozenset,
|
# subprotocols can be a set or frozenset,
|
||||||
# but ServerConnection needs a list
|
# but ServerProtocol needs a list
|
||||||
subprotocols = cast(
|
subprotocols = cast(
|
||||||
Optional[Sequence[Subprotocol]],
|
Optional[Sequence[Subprotocol]],
|
||||||
list(
|
list(
|
||||||
@@ -100,13 +111,13 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ws_conn = ServerConnection(
|
ws_proto = ServerProtocol(
|
||||||
max_size=self.websocket_max_size,
|
max_size=self.websocket_max_size,
|
||||||
subprotocols=subprotocols,
|
subprotocols=subprotocols,
|
||||||
state=OPEN,
|
state=OPEN,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
)
|
)
|
||||||
resp: "http11.Response" = ws_conn.accept(request)
|
resp: "http11.Response" = ws_proto.accept(request)
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = (
|
msg = (
|
||||||
"Failed to open a WebSocket connection.\n"
|
"Failed to open a WebSocket connection.\n"
|
||||||
@@ -129,7 +140,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
else:
|
else:
|
||||||
raise ServerError(resp.body, resp.status_code)
|
raise ServerError(resp.body, resp.status_code)
|
||||||
self.websocket = WebsocketImplProtocol(
|
self.websocket = WebsocketImplProtocol(
|
||||||
ws_conn,
|
ws_proto,
|
||||||
ping_interval=self.websocket_ping_interval,
|
ping_interval=self.websocket_ping_interval,
|
||||||
ping_timeout=self.websocket_ping_timeout,
|
ping_timeout=self.websocket_ping_timeout,
|
||||||
close_timeout=self.websocket_timeout,
|
close_timeout=self.websocket_timeout,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from signal import signal as signal_func
|
|||||||
from sanic.application.ext import setup_ext
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||||
from sanic.http.http3 import SessionTicketStore, get_config
|
from sanic.http.http3 import SessionTicketStore, get_config
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, server_logger
|
||||||
from sanic.models.server_types import Signal
|
from sanic.models.server_types import Signal
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
|
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
|
||||||
@@ -129,30 +129,33 @@ def _setup_system_signals(
|
|||||||
run_multiple: bool,
|
run_multiple: bool,
|
||||||
register_sys_signals: bool,
|
register_sys_signals: bool,
|
||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
) -> None:
|
) -> None: # no cov
|
||||||
|
print(">>>>>>>>>>>>>>>>>.", run_multiple)
|
||||||
# Ignore SIGINT when run_multiple
|
# Ignore SIGINT when run_multiple
|
||||||
if run_multiple:
|
if run_multiple:
|
||||||
signal_func(SIGINT, SIG_IGN)
|
signal_func(SIGINT, SIG_IGN)
|
||||||
os.environ["SANIC_WORKER_PROCESS"] = "true"
|
os.environ["SANIC_WORKER_PROCESS"] = "true"
|
||||||
|
|
||||||
# Register signals for graceful termination
|
# Register signals for graceful termination
|
||||||
if register_sys_signals:
|
if register_sys_signals and False:
|
||||||
if OS_IS_WINDOWS:
|
if OS_IS_WINDOWS:
|
||||||
ctrlc_workaround_for_windows(app)
|
ctrlc_workaround_for_windows(app)
|
||||||
else:
|
else:
|
||||||
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
|
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
|
||||||
loop.add_signal_handler(_signal, app.stop)
|
loop.add_signal_handler(
|
||||||
|
_signal, partial(app.stop, terminate=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
logger.info("Starting worker [%s]", pid)
|
server_logger.info("Starting worker [%s]", pid)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
logger.info("Stopping worker [%s]", pid)
|
server_logger.info("Stopping worker [%s]", pid)
|
||||||
|
|
||||||
loop.run_until_complete(before_stop())
|
loop.run_until_complete(before_stop())
|
||||||
|
|
||||||
@@ -161,6 +164,7 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
def _serve_http_1(
|
def _serve_http_1(
|
||||||
@@ -197,8 +201,12 @@ def _serve_http_1(
|
|||||||
asyncio_server_kwargs = (
|
asyncio_server_kwargs = (
|
||||||
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||||
)
|
)
|
||||||
|
if OS_IS_WINDOWS and sock:
|
||||||
|
pid = os.getpid()
|
||||||
|
sock = sock.share(pid)
|
||||||
|
sock = socket.fromshare(sock)
|
||||||
# UNIX sockets are always bound by us (to preserve semantics between modes)
|
# UNIX sockets are always bound by us (to preserve semantics between modes)
|
||||||
if unix:
|
elif unix:
|
||||||
sock = bind_unix_socket(unix, backlog=backlog)
|
sock = bind_unix_socket(unix, backlog=backlog)
|
||||||
server_coroutine = loop.create_server(
|
server_coroutine = loop.create_server(
|
||||||
server,
|
server,
|
||||||
@@ -222,6 +230,7 @@ def _serve_http_1(
|
|||||||
|
|
||||||
loop.run_until_complete(app._startup())
|
loop.run_until_complete(app._startup())
|
||||||
loop.run_until_complete(app._server_event("init", "before"))
|
loop.run_until_complete(app._server_event("init", "before"))
|
||||||
|
app.ack()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
@@ -299,6 +308,7 @@ def _serve_http_3(
|
|||||||
server = AsyncioServer(app, loop, coro, [])
|
server = AsyncioServer(app, loop, coro, [])
|
||||||
loop.run_until_complete(server.startup())
|
loop.run_until_complete(server.startup())
|
||||||
loop.run_until_complete(server.before_start())
|
loop.run_until_complete(server.before_start())
|
||||||
|
app.ack()
|
||||||
loop.run_until_complete(server)
|
loop.run_until_complete(server)
|
||||||
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
|
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
|
||||||
loop.run_until_complete(server.after_start())
|
loop.run_until_complete(server.after_start())
|
||||||
@@ -365,7 +375,9 @@ def serve_multiple(server_settings, workers):
|
|||||||
processes = []
|
processes = []
|
||||||
|
|
||||||
def sig_handler(signal, frame):
|
def sig_handler(signal, frame):
|
||||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
server_logger.info(
|
||||||
|
"Received signal %s. Shutting down.", Signals(signal).name
|
||||||
|
)
|
||||||
for process in processes:
|
for process in processes:
|
||||||
os.kill(process.pid, SIGTERM)
|
os.kill(process.pid, SIGTERM)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import socket
|
|||||||
import stat
|
import stat
|
||||||
|
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
from typing import Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from sanic.exceptions import ServerError
|
||||||
|
from sanic.http.constants import HTTP
|
||||||
|
|
||||||
|
|
||||||
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
|
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
|
||||||
@@ -16,6 +19,8 @@ def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
|
|||||||
:param backlog: Maximum number of connections to queue
|
:param backlog: Maximum number of connections to queue
|
||||||
:return: socket.socket object
|
:return: socket.socket object
|
||||||
"""
|
"""
|
||||||
|
location = (host, port)
|
||||||
|
# socket.share, socket.fromshare
|
||||||
try: # IP address: family must be specified for IPv6 at least
|
try: # IP address: family must be specified for IPv6 at least
|
||||||
ip = ip_address(host)
|
ip = ip_address(host)
|
||||||
host = str(ip)
|
host = str(ip)
|
||||||
@@ -25,8 +30,9 @@ def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
|
|||||||
except ValueError: # Hostname, may become AF_INET or AF_INET6
|
except ValueError: # Hostname, may become AF_INET or AF_INET6
|
||||||
sock = socket.socket()
|
sock = socket.socket()
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sock.bind((host, port))
|
sock.bind(location)
|
||||||
sock.listen(backlog)
|
sock.listen(backlog)
|
||||||
|
sock.set_inheritable(True)
|
||||||
return sock
|
return sock
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +42,7 @@ def bind_unix_socket(path: str, *, mode=0o666, backlog=100) -> socket.socket:
|
|||||||
:param backlog: Maximum number of connections to queue
|
:param backlog: Maximum number of connections to queue
|
||||||
:return: socket.socket object
|
:return: socket.socket object
|
||||||
"""
|
"""
|
||||||
"""Open or atomically replace existing socket with zero downtime."""
|
|
||||||
# Sanitise and pre-verify socket path
|
# Sanitise and pre-verify socket path
|
||||||
path = os.path.abspath(path)
|
path = os.path.abspath(path)
|
||||||
folder = os.path.dirname(path)
|
folder = os.path.dirname(path)
|
||||||
@@ -85,3 +91,40 @@ def remove_unix_socket(path: Optional[str]) -> None:
|
|||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def configure_socket(
|
||||||
|
server_settings: Dict[str, Any]
|
||||||
|
) -> Optional[socket.SocketType]:
|
||||||
|
# Create a listening socket or use the one in settings
|
||||||
|
if server_settings.get("version") is HTTP.VERSION_3:
|
||||||
|
return None
|
||||||
|
sock = server_settings.get("sock")
|
||||||
|
unix = server_settings["unix"]
|
||||||
|
backlog = server_settings["backlog"]
|
||||||
|
if unix:
|
||||||
|
sock = bind_unix_socket(unix, backlog=backlog)
|
||||||
|
server_settings["unix"] = unix
|
||||||
|
if sock is None:
|
||||||
|
try:
|
||||||
|
sock = bind_socket(
|
||||||
|
server_settings["host"],
|
||||||
|
server_settings["port"],
|
||||||
|
backlog=backlog,
|
||||||
|
)
|
||||||
|
except OSError as e: # no cov
|
||||||
|
error = ServerError(
|
||||||
|
f"Sanic server could not start: {e}.\n\n"
|
||||||
|
"This may have happened if you are running Sanic in the "
|
||||||
|
"global scope and not inside of a "
|
||||||
|
'`if __name__ == "__main__"` block.\n\nSee more information: '
|
||||||
|
"https://sanic.dev/en/guide/deployment/manager.html#"
|
||||||
|
"how-sanic-server-starts-processes\n"
|
||||||
|
)
|
||||||
|
error.quiet = True
|
||||||
|
raise error
|
||||||
|
sock.set_inheritable(True)
|
||||||
|
server_settings["sock"] = sock
|
||||||
|
server_settings["host"] = None
|
||||||
|
server_settings["port"] = None
|
||||||
|
return sock
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from sanic.exceptions import InvalidUsage
|
||||||
|
|
||||||
ASIMessage = MutableMapping[str, Any]
|
|
||||||
|
ASGIMessage = MutableMapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class WebSocketConnection:
|
class WebSocketConnection:
|
||||||
@@ -25,8 +27,8 @@ class WebSocketConnection:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
send: Callable[[ASIMessage], Awaitable[None]],
|
send: Callable[[ASGIMessage], Awaitable[None]],
|
||||||
receive: Callable[[], Awaitable[ASIMessage]],
|
receive: Callable[[], Awaitable[ASGIMessage]],
|
||||||
subprotocols: Optional[List[str]] = None,
|
subprotocols: Optional[List[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._send = send
|
self._send = send
|
||||||
@@ -43,11 +45,17 @@ class WebSocketConnection:
|
|||||||
|
|
||||||
await self._send(message)
|
await self._send(message)
|
||||||
|
|
||||||
async def recv(self, *args, **kwargs) -> Optional[str]:
|
async def recv(self, *args, **kwargs) -> Optional[Union[str, bytes]]:
|
||||||
message = await self._receive()
|
message = await self._receive()
|
||||||
|
|
||||||
if message["type"] == "websocket.receive":
|
if message["type"] == "websocket.receive":
|
||||||
return message["text"]
|
try:
|
||||||
|
return message["text"]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
return message["bytes"]
|
||||||
|
except KeyError:
|
||||||
|
raise InvalidUsage("Bad ASGI message received")
|
||||||
elif message["type"] == "websocket.disconnect":
|
elif message["type"] == "websocket.disconnect":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class WebsocketFrameAssembler:
|
|||||||
paused: bool
|
paused: bool
|
||||||
|
|
||||||
def __init__(self, protocol) -> None:
|
def __init__(self, protocol) -> None:
|
||||||
|
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
|
|
||||||
self.read_mutex = asyncio.Lock()
|
self.read_mutex = asyncio.Lock()
|
||||||
|
|||||||
@@ -12,21 +12,37 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN, Event
|
from websockets.exceptions import (
|
||||||
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
|
ConnectionClosed,
|
||||||
|
ConnectionClosedError,
|
||||||
|
ConnectionClosedOK,
|
||||||
|
)
|
||||||
from websockets.frames import Frame, Opcode
|
from websockets.frames import Frame, Opcode
|
||||||
from websockets.server import ServerConnection
|
|
||||||
|
|
||||||
|
try: # websockets < 11.0
|
||||||
|
from websockets.connection import Event, State
|
||||||
|
from websockets.server import ServerConnection as ServerProtocol
|
||||||
|
except ImportError: # websockets >= 11.0
|
||||||
|
from websockets.protocol import Event, State # type: ignore
|
||||||
|
from websockets.server import ServerProtocol # type: ignore
|
||||||
|
|
||||||
from websockets.typing import Data
|
from websockets.typing import Data
|
||||||
|
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import deprecation, error_logger, logger
|
||||||
from sanic.server.protocols.base_protocol import SanicProtocol
|
from sanic.server.protocols.base_protocol import SanicProtocol
|
||||||
|
|
||||||
from ...exceptions import ServerError, WebsocketClosed
|
from ...exceptions import ServerError, WebsocketClosed
|
||||||
from .frame import WebsocketFrameAssembler
|
from .frame import WebsocketFrameAssembler
|
||||||
|
|
||||||
|
|
||||||
|
OPEN = State.OPEN
|
||||||
|
CLOSING = State.CLOSING
|
||||||
|
CLOSED = State.CLOSED
|
||||||
|
|
||||||
|
|
||||||
class WebsocketImplProtocol:
|
class WebsocketImplProtocol:
|
||||||
connection: ServerConnection
|
ws_proto: ServerProtocol
|
||||||
io_proto: Optional[SanicProtocol]
|
io_proto: Optional[SanicProtocol]
|
||||||
loop: Optional[asyncio.AbstractEventLoop]
|
loop: Optional[asyncio.AbstractEventLoop]
|
||||||
max_queue: int
|
max_queue: int
|
||||||
@@ -52,14 +68,14 @@ class WebsocketImplProtocol:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
connection,
|
ws_proto,
|
||||||
max_queue=None,
|
max_queue=None,
|
||||||
ping_interval: Optional[float] = 20,
|
ping_interval: Optional[float] = 20,
|
||||||
ping_timeout: Optional[float] = 20,
|
ping_timeout: Optional[float] = 20,
|
||||||
close_timeout: float = 10,
|
close_timeout: float = 10,
|
||||||
loop=None,
|
loop=None,
|
||||||
):
|
):
|
||||||
self.connection = connection
|
self.ws_proto = ws_proto
|
||||||
self.io_proto = None
|
self.io_proto = None
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.max_queue = max_queue
|
self.max_queue = max_queue
|
||||||
@@ -81,7 +97,16 @@ class WebsocketImplProtocol:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def subprotocol(self):
|
def subprotocol(self):
|
||||||
return self.connection.subprotocol
|
return self.ws_proto.subprotocol
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection(self):
|
||||||
|
deprecation(
|
||||||
|
"The connection property has been deprecated and will be removed. "
|
||||||
|
"Please use the ws_proto property instead going forward.",
|
||||||
|
22.6,
|
||||||
|
)
|
||||||
|
return self.ws_proto
|
||||||
|
|
||||||
def pause_frames(self):
|
def pause_frames(self):
|
||||||
if not self.can_pause:
|
if not self.can_pause:
|
||||||
@@ -252,7 +277,7 @@ class WebsocketImplProtocol:
|
|||||||
|
|
||||||
def _force_disconnect(self) -> bool:
|
def _force_disconnect(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Internal methdod used by end_connection and fail_connection
|
Internal method used by end_connection and fail_connection
|
||||||
only when the graceful auto-closer cannot be used
|
only when the graceful auto-closer cannot be used
|
||||||
"""
|
"""
|
||||||
if self.auto_closer_task and not self.auto_closer_task.done():
|
if self.auto_closer_task and not self.auto_closer_task.done():
|
||||||
@@ -295,15 +320,15 @@ class WebsocketImplProtocol:
|
|||||||
# Not draining the write buffer is acceptable in this context.
|
# Not draining the write buffer is acceptable in this context.
|
||||||
|
|
||||||
# clear the send buffer
|
# clear the send buffer
|
||||||
_ = self.connection.data_to_send()
|
_ = self.ws_proto.data_to_send()
|
||||||
# If we're not already CLOSED or CLOSING, then send the close.
|
# If we're not already CLOSED or CLOSING, then send the close.
|
||||||
if self.connection.state is OPEN:
|
if self.ws_proto.state is OPEN:
|
||||||
if code in (1000, 1001):
|
if code in (1000, 1001):
|
||||||
self.connection.send_close(code, reason)
|
self.ws_proto.send_close(code, reason)
|
||||||
else:
|
else:
|
||||||
self.connection.fail(code, reason)
|
self.ws_proto.fail(code, reason)
|
||||||
try:
|
try:
|
||||||
data_to_send = self.connection.data_to_send()
|
data_to_send = self.ws_proto.data_to_send()
|
||||||
while (
|
while (
|
||||||
len(data_to_send)
|
len(data_to_send)
|
||||||
and self.io_proto
|
and self.io_proto
|
||||||
@@ -317,7 +342,7 @@ class WebsocketImplProtocol:
|
|||||||
...
|
...
|
||||||
if code == 1006:
|
if code == 1006:
|
||||||
# Special case: 1006 consider the transport already closed
|
# Special case: 1006 consider the transport already closed
|
||||||
self.connection.state = CLOSED
|
self.ws_proto.state = CLOSED
|
||||||
if self.data_finished_fut and not self.data_finished_fut.done():
|
if self.data_finished_fut and not self.data_finished_fut.done():
|
||||||
# We have a graceful auto-closer. Use it to close the connection.
|
# We have a graceful auto-closer. Use it to close the connection.
|
||||||
self.data_finished_fut.cancel()
|
self.data_finished_fut.cancel()
|
||||||
@@ -338,10 +363,10 @@ class WebsocketImplProtocol:
|
|||||||
# In Python Version 3.7: pause_reading is idempotent
|
# In Python Version 3.7: pause_reading is idempotent
|
||||||
# i.e. it can be called when the transport is already paused or closed.
|
# i.e. it can be called when the transport is already paused or closed.
|
||||||
self.io_proto.transport.pause_reading()
|
self.io_proto.transport.pause_reading()
|
||||||
if self.connection.state == OPEN:
|
if self.ws_proto.state == OPEN:
|
||||||
data_to_send = self.connection.data_to_send()
|
data_to_send = self.ws_proto.data_to_send()
|
||||||
self.connection.send_close(code, reason)
|
self.ws_proto.send_close(code, reason)
|
||||||
data_to_send.extend(self.connection.data_to_send())
|
data_to_send.extend(self.ws_proto.data_to_send())
|
||||||
try:
|
try:
|
||||||
while (
|
while (
|
||||||
len(data_to_send)
|
len(data_to_send)
|
||||||
@@ -450,7 +475,7 @@ class WebsocketImplProtocol:
|
|||||||
Raise ConnectionClosed in pending keepalive pings.
|
Raise ConnectionClosed in pending keepalive pings.
|
||||||
They'll never receive a pong once the connection is closed.
|
They'll never receive a pong once the connection is closed.
|
||||||
"""
|
"""
|
||||||
if self.connection.state is not CLOSED:
|
if self.ws_proto.state is not CLOSED:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
"Webscoket about_pings should only be called "
|
"Webscoket about_pings should only be called "
|
||||||
"after connection state is changed to CLOSED"
|
"after connection state is changed to CLOSED"
|
||||||
@@ -479,9 +504,9 @@ class WebsocketImplProtocol:
|
|||||||
self.fail_connection(code, reason)
|
self.fail_connection(code, reason)
|
||||||
return
|
return
|
||||||
async with self.conn_mutex:
|
async with self.conn_mutex:
|
||||||
if self.connection.state is OPEN:
|
if self.ws_proto.state is OPEN:
|
||||||
self.connection.send_close(code, reason)
|
self.ws_proto.send_close(code, reason)
|
||||||
data_to_send = self.connection.data_to_send()
|
data_to_send = self.ws_proto.data_to_send()
|
||||||
await self.send_data(data_to_send)
|
await self.send_data(data_to_send)
|
||||||
|
|
||||||
async def recv(self, timeout: Optional[float] = None) -> Optional[Data]:
|
async def recv(self, timeout: Optional[float] = None) -> Optional[Data]:
|
||||||
@@ -511,7 +536,7 @@ class WebsocketImplProtocol:
|
|||||||
"already waiting for the next message"
|
"already waiting for the next message"
|
||||||
)
|
)
|
||||||
await self.recv_lock.acquire()
|
await self.recv_lock.acquire()
|
||||||
if self.connection.state is CLOSED:
|
if self.ws_proto.state is CLOSED:
|
||||||
self.recv_lock.release()
|
self.recv_lock.release()
|
||||||
raise WebsocketClosed(
|
raise WebsocketClosed(
|
||||||
"Cannot receive from websocket interface after it is closed."
|
"Cannot receive from websocket interface after it is closed."
|
||||||
@@ -562,7 +587,7 @@ class WebsocketImplProtocol:
|
|||||||
"for the next message"
|
"for the next message"
|
||||||
)
|
)
|
||||||
await self.recv_lock.acquire()
|
await self.recv_lock.acquire()
|
||||||
if self.connection.state is CLOSED:
|
if self.ws_proto.state is CLOSED:
|
||||||
self.recv_lock.release()
|
self.recv_lock.release()
|
||||||
raise WebsocketClosed(
|
raise WebsocketClosed(
|
||||||
"Cannot receive from websocket interface after it is closed."
|
"Cannot receive from websocket interface after it is closed."
|
||||||
@@ -621,7 +646,7 @@ class WebsocketImplProtocol:
|
|||||||
"is already waiting for the next message"
|
"is already waiting for the next message"
|
||||||
)
|
)
|
||||||
await self.recv_lock.acquire()
|
await self.recv_lock.acquire()
|
||||||
if self.connection.state is CLOSED:
|
if self.ws_proto.state is CLOSED:
|
||||||
self.recv_lock.release()
|
self.recv_lock.release()
|
||||||
raise WebsocketClosed(
|
raise WebsocketClosed(
|
||||||
"Cannot receive from websocket interface after it is closed."
|
"Cannot receive from websocket interface after it is closed."
|
||||||
@@ -661,8 +686,7 @@ class WebsocketImplProtocol:
|
|||||||
:raises TypeError: for unsupported inputs
|
:raises TypeError: for unsupported inputs
|
||||||
"""
|
"""
|
||||||
async with self.conn_mutex:
|
async with self.conn_mutex:
|
||||||
|
if self.ws_proto.state in (CLOSED, CLOSING):
|
||||||
if self.connection.state in (CLOSED, CLOSING):
|
|
||||||
raise WebsocketClosed(
|
raise WebsocketClosed(
|
||||||
"Cannot write to websocket interface after it is closed."
|
"Cannot write to websocket interface after it is closed."
|
||||||
)
|
)
|
||||||
@@ -675,12 +699,12 @@ class WebsocketImplProtocol:
|
|||||||
# strings and bytes-like objects are iterable.
|
# strings and bytes-like objects are iterable.
|
||||||
|
|
||||||
if isinstance(message, str):
|
if isinstance(message, str):
|
||||||
self.connection.send_text(message.encode("utf-8"))
|
self.ws_proto.send_text(message.encode("utf-8"))
|
||||||
await self.send_data(self.connection.data_to_send())
|
await self.send_data(self.ws_proto.data_to_send())
|
||||||
|
|
||||||
elif isinstance(message, (bytes, bytearray, memoryview)):
|
elif isinstance(message, (bytes, bytearray, memoryview)):
|
||||||
self.connection.send_binary(message)
|
self.ws_proto.send_binary(message)
|
||||||
await self.send_data(self.connection.data_to_send())
|
await self.send_data(self.ws_proto.data_to_send())
|
||||||
|
|
||||||
elif isinstance(message, Mapping):
|
elif isinstance(message, Mapping):
|
||||||
# Catch a common mistake -- passing a dict to send().
|
# Catch a common mistake -- passing a dict to send().
|
||||||
@@ -709,7 +733,7 @@ class WebsocketImplProtocol:
|
|||||||
(which will be encoded to UTF-8) or a bytes-like object.
|
(which will be encoded to UTF-8) or a bytes-like object.
|
||||||
"""
|
"""
|
||||||
async with self.conn_mutex:
|
async with self.conn_mutex:
|
||||||
if self.connection.state in (CLOSED, CLOSING):
|
if self.ws_proto.state in (CLOSED, CLOSING):
|
||||||
raise WebsocketClosed(
|
raise WebsocketClosed(
|
||||||
"Cannot send a ping when the websocket interface "
|
"Cannot send a ping when the websocket interface "
|
||||||
"is closed."
|
"is closed."
|
||||||
@@ -737,8 +761,8 @@ class WebsocketImplProtocol:
|
|||||||
|
|
||||||
self.pings[data] = self.io_proto.loop.create_future()
|
self.pings[data] = self.io_proto.loop.create_future()
|
||||||
|
|
||||||
self.connection.send_ping(data)
|
self.ws_proto.send_ping(data)
|
||||||
await self.send_data(self.connection.data_to_send())
|
await self.send_data(self.ws_proto.data_to_send())
|
||||||
|
|
||||||
return asyncio.shield(self.pings[data])
|
return asyncio.shield(self.pings[data])
|
||||||
|
|
||||||
@@ -750,15 +774,15 @@ class WebsocketImplProtocol:
|
|||||||
be a string (which will be encoded to UTF-8) or a bytes-like object.
|
be a string (which will be encoded to UTF-8) or a bytes-like object.
|
||||||
"""
|
"""
|
||||||
async with self.conn_mutex:
|
async with self.conn_mutex:
|
||||||
if self.connection.state in (CLOSED, CLOSING):
|
if self.ws_proto.state in (CLOSED, CLOSING):
|
||||||
# Cannot send pong after transport is shutting down
|
# Cannot send pong after transport is shutting down
|
||||||
return
|
return
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.encode("utf-8")
|
data = data.encode("utf-8")
|
||||||
elif isinstance(data, (bytearray, memoryview)):
|
elif isinstance(data, (bytearray, memoryview)):
|
||||||
data = bytes(data)
|
data = bytes(data)
|
||||||
self.connection.send_pong(data)
|
self.ws_proto.send_pong(data)
|
||||||
await self.send_data(self.connection.data_to_send())
|
await self.send_data(self.ws_proto.data_to_send())
|
||||||
|
|
||||||
async def send_data(self, data_to_send):
|
async def send_data(self, data_to_send):
|
||||||
for data in data_to_send:
|
for data in data_to_send:
|
||||||
@@ -780,7 +804,7 @@ class WebsocketImplProtocol:
|
|||||||
SanicProtocol.close(self.io_proto, timeout=1.0)
|
SanicProtocol.close(self.io_proto, timeout=1.0)
|
||||||
|
|
||||||
async def async_data_received(self, data_to_send, events_to_process):
|
async def async_data_received(self, data_to_send, events_to_process):
|
||||||
if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0:
|
if self.ws_proto.state in (OPEN, CLOSING) and len(data_to_send) > 0:
|
||||||
# receiving data can generate data to send (eg, pong for a ping)
|
# receiving data can generate data to send (eg, pong for a ping)
|
||||||
# send connection.data_to_send()
|
# send connection.data_to_send()
|
||||||
await self.send_data(data_to_send)
|
await self.send_data(data_to_send)
|
||||||
@@ -788,9 +812,9 @@ class WebsocketImplProtocol:
|
|||||||
await self.process_events(events_to_process)
|
await self.process_events(events_to_process)
|
||||||
|
|
||||||
def data_received(self, data):
|
def data_received(self, data):
|
||||||
self.connection.receive_data(data)
|
self.ws_proto.receive_data(data)
|
||||||
data_to_send = self.connection.data_to_send()
|
data_to_send = self.ws_proto.data_to_send()
|
||||||
events_to_process = self.connection.events_received()
|
events_to_process = self.ws_proto.events_received()
|
||||||
if len(data_to_send) > 0 or len(events_to_process) > 0:
|
if len(data_to_send) > 0 or len(events_to_process) > 0:
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self.async_data_received(data_to_send, events_to_process)
|
self.async_data_received(data_to_send, events_to_process)
|
||||||
@@ -799,7 +823,7 @@ class WebsocketImplProtocol:
|
|||||||
async def async_eof_received(self, data_to_send, events_to_process):
|
async def async_eof_received(self, data_to_send, events_to_process):
|
||||||
# receiving EOF can generate data to send
|
# receiving EOF can generate data to send
|
||||||
# send connection.data_to_send()
|
# send connection.data_to_send()
|
||||||
if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0:
|
if self.ws_proto.state in (OPEN, CLOSING) and len(data_to_send) > 0:
|
||||||
await self.send_data(data_to_send)
|
await self.send_data(data_to_send)
|
||||||
if len(events_to_process) > 0:
|
if len(events_to_process) > 0:
|
||||||
await self.process_events(events_to_process)
|
await self.process_events(events_to_process)
|
||||||
@@ -819,9 +843,9 @@ class WebsocketImplProtocol:
|
|||||||
SanicProtocol.close(self.io_proto, timeout=1.0)
|
SanicProtocol.close(self.io_proto, timeout=1.0)
|
||||||
|
|
||||||
def eof_received(self) -> Optional[bool]:
|
def eof_received(self) -> Optional[bool]:
|
||||||
self.connection.receive_eof()
|
self.ws_proto.receive_eof()
|
||||||
data_to_send = self.connection.data_to_send()
|
data_to_send = self.ws_proto.data_to_send()
|
||||||
events_to_process = self.connection.events_received()
|
events_to_process = self.ws_proto.events_received()
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self.async_eof_received(data_to_send, events_to_process)
|
self.async_eof_received(data_to_send, events_to_process)
|
||||||
)
|
)
|
||||||
@@ -831,12 +855,19 @@ class WebsocketImplProtocol:
|
|||||||
"""
|
"""
|
||||||
The WebSocket Connection is Closed.
|
The WebSocket Connection is Closed.
|
||||||
"""
|
"""
|
||||||
if not self.connection.state == CLOSED:
|
if not self.ws_proto.state == CLOSED:
|
||||||
# signal to the websocket connection handler
|
# signal to the websocket connection handler
|
||||||
# we've lost the connection
|
# we've lost the connection
|
||||||
self.connection.fail(code=1006)
|
self.ws_proto.fail(code=1006)
|
||||||
self.connection.state = CLOSED
|
self.ws_proto.state = CLOSED
|
||||||
|
|
||||||
self.abort_pings()
|
self.abort_pings()
|
||||||
if self.connection_lost_waiter:
|
if self.connection_lost_waiter:
|
||||||
self.connection_lost_waiter.set_result(None)
|
self.connection_lost_waiter.set_result(None)
|
||||||
|
|
||||||
|
async def __aiter__(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
yield await self.recv()
|
||||||
|
except ConnectionClosedOK:
|
||||||
|
return
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class Event(Enum):
|
|||||||
HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
|
HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
|
||||||
HTTP_ROUTING_AFTER = "http.routing.after"
|
HTTP_ROUTING_AFTER = "http.routing.after"
|
||||||
HTTP_ROUTING_BEFORE = "http.routing.before"
|
HTTP_ROUTING_BEFORE = "http.routing.before"
|
||||||
|
HTTP_HANDLER_AFTER = "http.handler.after"
|
||||||
|
HTTP_HANDLER_BEFORE = "http.handler.before"
|
||||||
HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
|
HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
|
||||||
HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
|
HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
|
||||||
HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
|
HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
|
||||||
@@ -53,6 +55,8 @@ RESERVED_NAMESPACES = {
|
|||||||
Event.HTTP_LIFECYCLE_RESPONSE.value,
|
Event.HTTP_LIFECYCLE_RESPONSE.value,
|
||||||
Event.HTTP_ROUTING_AFTER.value,
|
Event.HTTP_ROUTING_AFTER.value,
|
||||||
Event.HTTP_ROUTING_BEFORE.value,
|
Event.HTTP_ROUTING_BEFORE.value,
|
||||||
|
Event.HTTP_HANDLER_AFTER.value,
|
||||||
|
Event.HTTP_HANDLER_BEFORE.value,
|
||||||
Event.HTTP_LIFECYCLE_SEND.value,
|
Event.HTTP_LIFECYCLE_SEND.value,
|
||||||
Event.HTTP_MIDDLEWARE_AFTER.value,
|
Event.HTTP_MIDDLEWARE_AFTER.value,
|
||||||
Event.HTTP_MIDDLEWARE_BEFORE.value,
|
Event.HTTP_MIDDLEWARE_BEFORE.value,
|
||||||
@@ -150,13 +154,11 @@ class SignalRouter(BaseRouter):
|
|||||||
try:
|
try:
|
||||||
for signal in signals:
|
for signal in signals:
|
||||||
params.pop("__trigger__", None)
|
params.pop("__trigger__", None)
|
||||||
|
requirements = signal.extra.requirements
|
||||||
if (
|
if (
|
||||||
(condition is None and signal.ctx.exclusive is False)
|
(condition is None and signal.ctx.exclusive is False)
|
||||||
or (
|
or (condition is None and not requirements)
|
||||||
condition is None
|
or (condition == requirements)
|
||||||
and not signal.handler.__requirements__
|
|
||||||
)
|
|
||||||
or (condition == signal.handler.__requirements__)
|
|
||||||
) and (signal.ctx.trigger or event == signal.ctx.definition):
|
) and (signal.ctx.trigger or event == signal.ctx.definition):
|
||||||
maybe_coroutine = signal.handler(**params)
|
maybe_coroutine = signal.handler(**params)
|
||||||
if isawaitable(maybe_coroutine):
|
if isawaitable(maybe_coroutine):
|
||||||
@@ -187,7 +189,7 @@ class SignalRouter(BaseRouter):
|
|||||||
fail_not_found=fail_not_found and inline,
|
fail_not_found=fail_not_found and inline,
|
||||||
reverse=reverse,
|
reverse=reverse,
|
||||||
)
|
)
|
||||||
logger.debug(f"Dispatching signal: {event}")
|
logger.debug(f"Dispatching signal: {event}", extra={"verbosity": 1})
|
||||||
|
|
||||||
if inline:
|
if inline:
|
||||||
return await dispatch
|
return await dispatch
|
||||||
@@ -215,8 +217,13 @@ class SignalRouter(BaseRouter):
|
|||||||
if not trigger:
|
if not trigger:
|
||||||
event = ".".join([*parts[:2], "<__trigger__>"])
|
event = ".".join([*parts[:2], "<__trigger__>"])
|
||||||
|
|
||||||
handler.__requirements__ = condition # type: ignore
|
try:
|
||||||
handler.__trigger__ = trigger # type: ignore
|
# 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,
|
||||||
@@ -228,6 +235,7 @@ class SignalRouter(BaseRouter):
|
|||||||
signal.ctx.exclusive = exclusive
|
signal.ctx.exclusive = exclusive
|
||||||
signal.ctx.trigger = trigger
|
signal.ctx.trigger = trigger
|
||||||
signal.ctx.definition = event_definition
|
signal.ctx.definition = event_definition
|
||||||
|
signal.extra.requirements = condition
|
||||||
|
|
||||||
return cast(Signal, signal)
|
return cast(Signal, signal)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.response import redirect
|
|
||||||
|
|
||||||
|
|
||||||
def create_simple_server(directory: Path):
|
def create_simple_server(directory: Path):
|
||||||
@@ -12,10 +11,8 @@ def create_simple_server(directory: Path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
app = Sanic("SimpleServer")
|
app = Sanic("SimpleServer")
|
||||||
app.static("/", directory, name="main")
|
app.static(
|
||||||
|
"/", directory, name="main", directory_view=True, index="index.html"
|
||||||
@app.get("/")
|
)
|
||||||
def index(_):
|
|
||||||
return redirect(app.url_for("main", filename="index.html"))
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ class TouchUpMeta(SanicMeta):
|
|||||||
methods = attrs.get("__touchup__")
|
methods = attrs.get("__touchup__")
|
||||||
attrs["__touched__"] = False
|
attrs["__touched__"] = False
|
||||||
if methods:
|
if methods:
|
||||||
|
|
||||||
for method in methods:
|
for method in methods:
|
||||||
if method not in attrs:
|
if method not in attrs:
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
|
|||||||
57
sanic/types/shared_ctx.py
Normal file
57
sanic/types/shared_ctx.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
from sanic.log import Colors, error_logger
|
||||||
|
|
||||||
|
|
||||||
|
class SharedContext(SimpleNamespace):
|
||||||
|
SAFE = ("_lock",)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._lock = False
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
|
if self.is_locked:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Cannot set {name} on locked SharedContext object"
|
||||||
|
)
|
||||||
|
if not os.environ.get("SANIC_WORKER_NAME"):
|
||||||
|
to_check: Iterable[Any]
|
||||||
|
if not isinstance(value, (tuple, frozenset)):
|
||||||
|
to_check = [value]
|
||||||
|
else:
|
||||||
|
to_check = value
|
||||||
|
for item in to_check:
|
||||||
|
self._check(name, item)
|
||||||
|
super().__setattr__(name, value)
|
||||||
|
|
||||||
|
def _check(self, name: str, value: Any) -> None:
|
||||||
|
if name in self.SAFE:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
module = value.__module__
|
||||||
|
except AttributeError:
|
||||||
|
module = ""
|
||||||
|
if not any(
|
||||||
|
module.startswith(prefix)
|
||||||
|
for prefix in ("multiprocessing", "ctypes")
|
||||||
|
):
|
||||||
|
error_logger.warning(
|
||||||
|
f"{Colors.YELLOW}Unsafe object {Colors.PURPLE}{name} "
|
||||||
|
f"{Colors.YELLOW}with type {Colors.PURPLE}{type(value)} "
|
||||||
|
f"{Colors.YELLOW}was added to shared_ctx. It may not "
|
||||||
|
"not function as intended. Consider using the regular "
|
||||||
|
f"ctx.\nFor more information, please see https://sanic.dev/en"
|
||||||
|
"/guide/deployment/manager.html#using-shared-context-between-"
|
||||||
|
f"worker-processes.{Colors.END}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_locked(self) -> bool:
|
||||||
|
return getattr(self, "_lock", False)
|
||||||
|
|
||||||
|
def lock(self) -> None:
|
||||||
|
self._lock = True
|
||||||
@@ -75,7 +75,6 @@ def load_module_from_file_location(
|
|||||||
location = location.decode(encoding)
|
location = location.decode(encoding)
|
||||||
|
|
||||||
if isinstance(location, Path) or "/" in location or "$" in location:
|
if isinstance(location, Path) or "/" in location or "$" in location:
|
||||||
|
|
||||||
if not isinstance(location, Path):
|
if not isinstance(location, Path):
|
||||||
# A) Check if location contains any environment variables
|
# A) Check if location contains any environment variables
|
||||||
# in format ${some_env_var}.
|
# in format ${some_env_var}.
|
||||||
|
|||||||
0
sanic/worker/__init__.py
Normal file
0
sanic/worker/__init__.py
Normal file
18
sanic/worker/constants.py
Normal file
18
sanic/worker/constants.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from enum import IntEnum, auto
|
||||||
|
|
||||||
|
from sanic.compat import UpperStrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class RestartOrder(UpperStrEnum):
|
||||||
|
SHUTDOWN_FIRST = auto()
|
||||||
|
STARTUP_FIRST = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessState(IntEnum):
|
||||||
|
IDLE = auto()
|
||||||
|
RESTARTING = auto()
|
||||||
|
STARTING = auto()
|
||||||
|
STARTED = auto()
|
||||||
|
ACKED = auto()
|
||||||
|
JOINED = auto()
|
||||||
|
TERMINATED = auto()
|
||||||
123
sanic/worker/inspector.py
Normal file
123
sanic/worker/inspector.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from inspect import isawaitable
|
||||||
|
from multiprocessing.connection import Connection
|
||||||
|
from os import environ
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Mapping, Union
|
||||||
|
|
||||||
|
from sanic.exceptions import Unauthorized
|
||||||
|
from sanic.helpers import Default
|
||||||
|
from sanic.log import logger
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
|
class Inspector:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
publisher: Connection,
|
||||||
|
app_info: Dict[str, Any],
|
||||||
|
worker_state: Mapping[str, Any],
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
api_key: str,
|
||||||
|
tls_key: Union[Path, str, Default],
|
||||||
|
tls_cert: Union[Path, str, Default],
|
||||||
|
):
|
||||||
|
self._publisher = publisher
|
||||||
|
self.app_info = app_info
|
||||||
|
self.worker_state = worker_state
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.api_key = api_key
|
||||||
|
self.tls_key = tls_key
|
||||||
|
self.tls_cert = tls_cert
|
||||||
|
|
||||||
|
def __call__(self, run=True, **_) -> Inspector:
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
self.app = Sanic("Inspector")
|
||||||
|
self._setup()
|
||||||
|
if run:
|
||||||
|
self.app.run(
|
||||||
|
host=self.host,
|
||||||
|
port=self.port,
|
||||||
|
single_process=True,
|
||||||
|
ssl={"key": self.tls_key, "cert": self.tls_cert}
|
||||||
|
if not isinstance(self.tls_key, Default)
|
||||||
|
and not isinstance(self.tls_cert, Default)
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
self.app.get("/")(self._info)
|
||||||
|
self.app.post("/<action:str>")(self._action)
|
||||||
|
if self.api_key:
|
||||||
|
self.app.on_request(self._authentication)
|
||||||
|
environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "true"
|
||||||
|
|
||||||
|
def _authentication(self, request: Request) -> None:
|
||||||
|
if request.token != self.api_key:
|
||||||
|
raise Unauthorized("Bad API key")
|
||||||
|
|
||||||
|
async def _action(self, request: Request, action: str):
|
||||||
|
logger.info("Incoming inspector action: %s", action)
|
||||||
|
output: Any = None
|
||||||
|
method = getattr(self, action, None)
|
||||||
|
if method:
|
||||||
|
kwargs = {}
|
||||||
|
if request.body:
|
||||||
|
kwargs = request.json
|
||||||
|
args = kwargs.pop("args", ())
|
||||||
|
output = method(*args, **kwargs)
|
||||||
|
if isawaitable(output):
|
||||||
|
output = await output
|
||||||
|
|
||||||
|
return await self._respond(request, output)
|
||||||
|
|
||||||
|
async def _info(self, request: Request):
|
||||||
|
return await self._respond(request, self._state_to_json())
|
||||||
|
|
||||||
|
async def _respond(self, request: Request, output: Any):
|
||||||
|
name = request.match_info.get("action", "info")
|
||||||
|
return json(
|
||||||
|
{"meta": {"action": name}, "result": output},
|
||||||
|
escape_forward_slashes=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _state_to_json(self) -> Dict[str, Any]:
|
||||||
|
output = {"info": self.app_info}
|
||||||
|
output["workers"] = self._make_safe(dict(self.worker_state))
|
||||||
|
return output
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_safe(obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
for key, value in obj.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
obj[key] = Inspector._make_safe(value)
|
||||||
|
elif isinstance(value, datetime):
|
||||||
|
obj[key] = value.isoformat()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def reload(self, zero_downtime: bool = False) -> None:
|
||||||
|
message = "__ALL_PROCESSES__:"
|
||||||
|
if zero_downtime:
|
||||||
|
message += ":STARTUP_FIRST"
|
||||||
|
self._publisher.send(message)
|
||||||
|
|
||||||
|
def scale(self, replicas) -> str:
|
||||||
|
num_workers = 1
|
||||||
|
if replicas:
|
||||||
|
num_workers = int(replicas)
|
||||||
|
log_msg = f"Scaling to {num_workers}"
|
||||||
|
logger.info(log_msg)
|
||||||
|
message = f"__SCALE__:{num_workers}"
|
||||||
|
self._publisher.send(message)
|
||||||
|
return log_msg
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
message = "__TERMINATE__"
|
||||||
|
self._publisher.send(message)
|
||||||
127
sanic/worker/loader.py
Normal file
127
sanic/worker/loader.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
|
||||||
|
|
||||||
|
from sanic.http.tls.context import process_to_context
|
||||||
|
from sanic.http.tls.creators import MkcertCreator, TrustmeCreator
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sanic import Sanic as SanicApp
|
||||||
|
|
||||||
|
|
||||||
|
class AppLoader:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
module_input: str = "",
|
||||||
|
as_factory: bool = False,
|
||||||
|
as_simple: bool = False,
|
||||||
|
args: Any = None,
|
||||||
|
factory: Optional[Callable[[], SanicApp]] = None,
|
||||||
|
) -> None:
|
||||||
|
self.module_input = module_input
|
||||||
|
self.module_name = ""
|
||||||
|
self.app_name = ""
|
||||||
|
self.as_factory = as_factory
|
||||||
|
self.as_simple = as_simple
|
||||||
|
self.args = args
|
||||||
|
self.factory = factory
|
||||||
|
self.cwd = os.getcwd()
|
||||||
|
|
||||||
|
if module_input:
|
||||||
|
delimiter = ":" if ":" in module_input else "."
|
||||||
|
if module_input.count(delimiter):
|
||||||
|
module_name, app_name = module_input.rsplit(delimiter, 1)
|
||||||
|
self.module_name = module_name
|
||||||
|
self.app_name = app_name
|
||||||
|
if self.app_name.endswith("()"):
|
||||||
|
self.as_factory = True
|
||||||
|
self.app_name = self.app_name[:-2]
|
||||||
|
|
||||||
|
def load(self) -> SanicApp:
|
||||||
|
module_path = os.path.abspath(self.cwd)
|
||||||
|
if module_path not in sys.path:
|
||||||
|
sys.path.append(module_path)
|
||||||
|
|
||||||
|
if self.factory:
|
||||||
|
return self.factory()
|
||||||
|
else:
|
||||||
|
from sanic.app import Sanic
|
||||||
|
from sanic.simple import create_simple_server
|
||||||
|
|
||||||
|
if self.as_simple:
|
||||||
|
path = Path(self.module_input)
|
||||||
|
app = create_simple_server(path)
|
||||||
|
else:
|
||||||
|
if self.module_name == "" and os.path.isdir(self.module_input):
|
||||||
|
raise ValueError(
|
||||||
|
"App not found.\n"
|
||||||
|
" Please use --simple if you are passing a "
|
||||||
|
"directory to sanic.\n"
|
||||||
|
f" eg. sanic {self.module_input} --simple"
|
||||||
|
)
|
||||||
|
|
||||||
|
module = import_module(self.module_name)
|
||||||
|
app = getattr(module, self.app_name, None)
|
||||||
|
if self.as_factory:
|
||||||
|
try:
|
||||||
|
app = app(self.args)
|
||||||
|
except TypeError:
|
||||||
|
app = app()
|
||||||
|
|
||||||
|
app_type_name = type(app).__name__
|
||||||
|
|
||||||
|
if (
|
||||||
|
not isinstance(app, Sanic)
|
||||||
|
and self.args
|
||||||
|
and hasattr(self.args, "module")
|
||||||
|
):
|
||||||
|
if callable(app):
|
||||||
|
solution = f"sanic {self.args.module} --factory"
|
||||||
|
raise ValueError(
|
||||||
|
"Module is not a Sanic app, it is a "
|
||||||
|
f"{app_type_name}\n"
|
||||||
|
" If this callable returns a "
|
||||||
|
f"Sanic instance try: \n{solution}"
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"Module is not a Sanic app, it is a {app_type_name}\n"
|
||||||
|
f" Perhaps you meant {self.args.module}:app?"
|
||||||
|
)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
class CertLoader:
|
||||||
|
_creators = {
|
||||||
|
"mkcert": MkcertCreator,
|
||||||
|
"trustme": TrustmeCreator,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, ssl_data: Dict[str, Union[str, os.PathLike]]):
|
||||||
|
self._ssl_data = ssl_data
|
||||||
|
|
||||||
|
creator_name = cast(str, ssl_data.get("creator"))
|
||||||
|
|
||||||
|
self._creator_class = self._creators.get(creator_name)
|
||||||
|
if not creator_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._creator_class:
|
||||||
|
raise RuntimeError(f"Unknown certificate creator: {creator_name}")
|
||||||
|
|
||||||
|
self._key = ssl_data["key"]
|
||||||
|
self._cert = ssl_data["cert"]
|
||||||
|
self._localhost = cast(str, ssl_data["localhost"])
|
||||||
|
|
||||||
|
def load(self, app: SanicApp):
|
||||||
|
if not self._creator_class:
|
||||||
|
return process_to_context(self._ssl_data)
|
||||||
|
|
||||||
|
creator = self._creator_class(app, self._key, self._cert)
|
||||||
|
return creator.generate_cert(self._localhost)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user