Compare commits
No commits in common. "ruff-only" and "main" have entirely different histories.
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Sanic documentation build configuration file, created by
|
# Sanic documentation build configuration file, created by
|
||||||
# sphinx-quickstart on Sun Dec 25 18:07:21 2016.
|
# sphinx-quickstart on Sun Dec 25 18:07:21 2016.
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
# Add support for auto-doc
|
# Add support for auto-doc
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +19,8 @@ import sys
|
||||||
root_directory = os.path.dirname(os.getcwd())
|
root_directory = os.path.dirname(os.getcwd())
|
||||||
sys.path.insert(0, root_directory)
|
sys.path.insert(0, root_directory)
|
||||||
|
|
||||||
import sanic # noqa: E402
|
import sanic
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from random import randint
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,6 +25,5 @@ def key_exist_handler(request):
|
||||||
|
|
||||||
return text("num does not exist in request")
|
return text("num does not exist in request")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from sanic import Blueprint, Sanic
|
from sanic import Blueprint, Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Demonstrates that blueprint request middleware are executed in the order they
|
Demonstrates that blueprint request middleware are executed in the order they
|
||||||
are added. And blueprint response middleware are executed in _reverse_ order.
|
are added. And blueprint response middleware are executed in _reverse_ order.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from sanic import Blueprint, Sanic
|
from sanic import Blueprint, Sanic
|
||||||
from sanic.response import file, json
|
from sanic.response import file, json
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
|
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
|
||||||
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
|
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
|
||||||
|
|
|
@ -2,6 +2,7 @@ from asyncio import sleep
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("DelayedResponseApp", strict_slashes=True)
|
app = Sanic("DelayedResponseApp", strict_slashes=True)
|
||||||
app.config.AUTO_EXTEND = False
|
app.config.AUTO_EXTEND = False
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ an external service.
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Imports and code relevant for our CustomHandler class
|
Imports and code relevant for our CustomHandler class
|
||||||
(Ordinarily this would be in a separate file)
|
(Ordinarily this would be in a separate file)
|
||||||
|
@ -38,6 +39,7 @@ server's error_handler to an instance of our CustomHandler
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
handler = CustomHandler()
|
handler = CustomHandler()
|
||||||
app = Sanic("Example", error_handler=handler)
|
app = Sanic("Example", error_handler=handler)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from sanic import Sanic, response, text
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
|
|
||||||
|
|
||||||
HTTP_PORT = 9999
|
HTTP_PORT = 9999
|
||||||
HTTPS_PORT = 8888
|
HTTPS_PORT = 8888
|
||||||
|
|
||||||
|
@ -35,7 +36,9 @@ def proxy(request, path):
|
||||||
|
|
||||||
@https.main_process_start
|
@https.main_process_start
|
||||||
async def start(app, _):
|
async def start(app, _):
|
||||||
http_server = await http.create_server(port=HTTP_PORT, return_asyncio_server=True)
|
http_server = await http.create_server(
|
||||||
|
port=HTTP_PORT, return_asyncio_server=True
|
||||||
|
)
|
||||||
app.add_task(runner(http, http_server))
|
app.add_task(runner(http, http_server))
|
||||||
app.ctx.http_server = http_server
|
app.ctx.http_server = http_server
|
||||||
app.ctx.http = http
|
app.ctx.http = http
|
||||||
|
@ -66,6 +69,5 @@ async def runner(app: Sanic, app_server: AsyncioServer):
|
||||||
app.is_running = False
|
app.is_running = False
|
||||||
app.is_stopping = True
|
app.is_stopping = True
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
https.run(port=HTTPS_PORT, debug=True)
|
https.run(port=HTTPS_PORT, debug=True)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import httpx
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
sem = None
|
sem = None
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from platform import node
|
from platform import node
|
||||||
from uuid import getnode as get_mac
|
from uuid import getnode as get_mac
|
||||||
|
@ -10,6 +11,7 @@ from sanic import Sanic
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger("logdna")
|
log = logging.getLogger("logdna")
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
@ -33,7 +35,9 @@ logdna_options = {
|
||||||
"mac": get_mac_address(),
|
"mac": get_mac_address(),
|
||||||
}
|
}
|
||||||
|
|
||||||
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
|
logdna_handler = LogDNAHandler(
|
||||||
|
getenv("LOGDNA_API_KEY"), options=logdna_options
|
||||||
|
)
|
||||||
|
|
||||||
logdna = logging.getLogger(__name__)
|
logdna = logging.getLogger(__name__)
|
||||||
logdna.setLevel(logging.INFO)
|
logdna.setLevel(logging.INFO)
|
||||||
|
@ -44,7 +48,7 @@ app = Sanic("Example")
|
||||||
|
|
||||||
@app.middleware
|
@app.middleware
|
||||||
def log_request(request: Request):
|
def log_request(request: Request):
|
||||||
logdna.info(f"I was Here with a new Request to URL: {request.url}")
|
logdna.info("I was Here with a new Request to URL: {}".format(request.url))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
|
@ -4,6 +4,7 @@ Modify header or status in response
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
|
|
||||||
from sanic import Sanic, text
|
from sanic import Sanic, text
|
||||||
|
|
||||||
|
|
||||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||||
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||||
logging_format += "%(message)s"
|
logging_format += "%(message)s"
|
||||||
|
|
|
@ -11,6 +11,7 @@ Run with xdist params:
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic_testing import SanicTestClient
|
from sanic_testing import SanicTestClient
|
||||||
from sanic_testing.testing import PORT as PORT_BASE
|
from sanic_testing.testing import PORT as PORT_BASE
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from sanic.response import stream, text
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
from sanic.views import stream as stream_decorator
|
from sanic.views import stream as stream_decorator
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("bp_example")
|
bp = Blueprint("bp_example")
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from sanic import Sanic, response
|
||||||
from sanic.config import Config
|
from sanic.config import Config
|
||||||
from sanic.exceptions import RequestTimeout
|
from sanic.exceptions import RequestTimeout
|
||||||
|
|
||||||
|
|
||||||
Config.REQUEST_TIMEOUT = 1
|
Config.REQUEST_TIMEOUT = 1
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ from sanic import Sanic
|
||||||
from sanic.exceptions import SanicException
|
from sanic.exceptions import SanicException
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
|
|
||||||
|
|
||||||
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,7 +43,9 @@ async def handler_file(request):
|
||||||
|
|
||||||
@app.route("/file_stream")
|
@app.route("/file_stream")
|
||||||
async def handler_file_stream(request):
|
async def handler_file_stream(request):
|
||||||
return await response.file_stream(Path("../") / "setup.py", chunk_size=1024)
|
return await response.file_stream(
|
||||||
|
Path("../") / "setup.py", chunk_size=1024
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/stream", stream=True)
|
@app.post("/stream", stream=True)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import uvloop
|
||||||
|
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from signal import SIGINT, signal
|
from signal import SIGINT, signal
|
||||||
|
|
||||||
import uvloop
|
import uvloop
|
||||||
|
@ -6,6 +7,7 @@ import uvloop
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
from sanic.server import AsyncioServer
|
from sanic.server import AsyncioServer
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,10 +35,11 @@ async def after_server_stop(app, loop):
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return response.json({"answer": "42"})
|
return response.json({"answer": "42"})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
serv_coro = app.create_server(
|
||||||
|
host="0.0.0.0", port=8000, return_asyncio_server=True
|
||||||
|
)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
||||||
signal(SIGINT, lambda s, f: loop.stop())
|
signal(SIGINT, lambda s, f: loop.stop())
|
||||||
|
|
|
@ -6,6 +6,7 @@ from sentry_sdk.integrations.sanic import SanicIntegration
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
sentry_init(
|
sentry_init(
|
||||||
dsn=getenv("SENTRY_DSN"),
|
dsn=getenv("SENTRY_DSN"),
|
||||||
integrations=[SanicIntegration()],
|
integrations=[SanicIntegration()],
|
||||||
|
|
|
@ -2,6 +2,7 @@ from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("some_name")
|
app = Sanic("some_name")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
app.static("/", "./static")
|
app.static("/", "./static")
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic import response as res
|
from sanic import response as res
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from sanic import Sanic, response
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.log import logger as log
|
from sanic.log import logger as log
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ def test_sync(request):
|
||||||
|
|
||||||
@app.route("/dynamic/<name>/<i:int>")
|
@app.route("/dynamic/<name>/<i:int>")
|
||||||
def test_params(request, name, i):
|
def test_params(request, name, i):
|
||||||
return response.text(f"yeehaww {name} {i}")
|
return response.text("yeehaww {} {}".format(name, i))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/exception")
|
@app.route("/exception")
|
||||||
|
@ -42,7 +43,9 @@ async def test_file(request):
|
||||||
|
|
||||||
@app.route("/file_stream")
|
@app.route("/file_stream")
|
||||||
async def test_file_stream(request):
|
async def test_file_stream(request):
|
||||||
return await response.file_stream(os.path.abspath("setup.py"), chunk_size=1024)
|
return await response.file_stream(
|
||||||
|
os.path.abspath("setup.py"), chunk_size=1024
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ async def index(request):
|
||||||
|
|
||||||
@app.route("/posts/<post_id>")
|
@app.route("/posts/<post_id>")
|
||||||
async def post_handler(request, post_id):
|
async def post_handler(request, post_id):
|
||||||
return response.text(f"Post - {post_id}")
|
return response.text("Post - {}".format(post_id))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -2,6 +2,7 @@ from sanic import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
app = Sanic(name="blue-print-group-version-example")
|
app = Sanic(name="blue-print-group-version-example")
|
||||||
|
|
||||||
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
|
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from sanic import Sanic, response
|
from sanic import Sanic, response
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
# curl -H "Host: example.com" localhost:8000
|
# curl -H "Host: example.com" localhost:8000
|
||||||
# curl -H "Host: sub.example.com" localhost:8000
|
# curl -H "Host: sub.example.com" localhost:8000
|
||||||
|
@ -11,7 +12,9 @@ app = Sanic("Example")
|
||||||
bp = Blueprint("bp", host="bp.example.com")
|
bp = Blueprint("bp", host="bp.example.com")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", host=["example.com", "somethingelse.com", "therestofyourdomains.com"])
|
@app.route(
|
||||||
|
"/", host=["example.com", "somethingelse.com", "therestofyourdomains.com"]
|
||||||
|
)
|
||||||
async def hello_0(request):
|
async def hello_0(request):
|
||||||
return response.text("Some defaults")
|
return response.text("Some defaults")
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import redirect
|
from sanic.response import redirect
|
||||||
|
|
||||||
|
|
||||||
app = Sanic("Example")
|
app = Sanic("Example")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,14 @@ from emoji import EMOJI
|
||||||
COLUMN_PATTERN = re.compile(r"---:1\s*(.*?)\s*:--:1\s*(.*?)\s*:---", re.DOTALL)
|
COLUMN_PATTERN = re.compile(r"---:1\s*(.*?)\s*:--:1\s*(.*?)\s*:---", re.DOTALL)
|
||||||
PYTHON_HIGHLIGHT_PATTERN = re.compile(r"```python\{+.*?\}", re.DOTALL)
|
PYTHON_HIGHLIGHT_PATTERN = re.compile(r"```python\{+.*?\}", re.DOTALL)
|
||||||
BASH_HIGHLIGHT_PATTERN = re.compile(r"```bash\{+.*?\}", re.DOTALL)
|
BASH_HIGHLIGHT_PATTERN = re.compile(r"```bash\{+.*?\}", re.DOTALL)
|
||||||
NOTIFICATION_PATTERN = re.compile(r":::\s*(\w+)\s*(.*?)\n([\s\S]*?):::", re.MULTILINE)
|
NOTIFICATION_PATTERN = re.compile(
|
||||||
|
r":::\s*(\w+)\s*(.*?)\n([\s\S]*?):::", re.MULTILINE
|
||||||
|
)
|
||||||
EMOJI_PATTERN = re.compile(r":(\w+):")
|
EMOJI_PATTERN = re.compile(r":(\w+):")
|
||||||
CURRENT_DIR = Path(__file__).parent
|
CURRENT_DIR = Path(__file__).parent
|
||||||
SOURCE_DIR = CURRENT_DIR.parent.parent.parent.parent / "sanic-guide" / "src" / "en"
|
SOURCE_DIR = (
|
||||||
|
CURRENT_DIR.parent.parent.parent.parent / "sanic-guide" / "src" / "en"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_columns(content: str):
|
def convert_columns(content: str):
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[tool.ruff]
|
|
||||||
extend = "../pyproject.toml"
|
|
||||||
|
|
||||||
[tool.ruff.isort]
|
|
||||||
known-first-party = ["webapp"]
|
|
|
@ -13,7 +13,9 @@ def do_footer(builder: Builder, request: Request) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _pagination(request: Request) -> Builder:
|
def _pagination(request: Request) -> Builder:
|
||||||
return E.div(_pagination_left(request), _pagination_right(request), class_="level")
|
return E.div(
|
||||||
|
_pagination_left(request), _pagination_right(request), class_="level"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _pagination_left(request: Request) -> Builder:
|
def _pagination_left(request: Request) -> Builder:
|
||||||
|
@ -62,7 +64,9 @@ def _content() -> Builder:
|
||||||
href="https://github.com/sanic-org/sanic/blob/master/LICENSE",
|
href="https://github.com/sanic-org/sanic/blob/master/LICENSE",
|
||||||
target="_blank",
|
target="_blank",
|
||||||
rel="nofollow noopener noreferrer",
|
rel="nofollow noopener noreferrer",
|
||||||
).br()(E.small(f"Copyright © 2018-{year} Sanic Community Organization")),
|
).br()(
|
||||||
|
E.small(f"Copyright © 2018-{year} Sanic Community Organization")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return E.div(
|
return E.div(
|
||||||
inner,
|
inner,
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
|
from webapp.display.layouts.models import MenuItem
|
||||||
|
|
||||||
from html5tagger import Builder, E # type: ignore
|
from html5tagger import Builder, E # type: ignore
|
||||||
from sanic import Request
|
from sanic import Request
|
||||||
|
|
||||||
from webapp.display.layouts.models import MenuItem
|
|
||||||
|
|
||||||
|
|
||||||
def do_navbar(builder: Builder, request: Request) -> None:
|
def do_navbar(builder: Builder, request: Request) -> None:
|
||||||
navbar_items = [
|
navbar_items = [
|
||||||
_render_navbar_item(item, request) for item in request.app.config.NAVBAR
|
_render_navbar_item(item, request)
|
||||||
|
for item in request.app.config.NAVBAR
|
||||||
]
|
]
|
||||||
container = E.div(_search_form(request), *navbar_items, class_="navbar-end")
|
container = E.div(
|
||||||
|
_search_form(request), *navbar_items, class_="navbar-end"
|
||||||
|
)
|
||||||
|
|
||||||
builder.nav(
|
builder.nav(
|
||||||
E.div(container, class_="navbar-menu"),
|
E.div(container, class_="navbar-menu"),
|
||||||
|
@ -43,7 +46,10 @@ def _render_navbar_item(item: MenuItem, request: Request) -> Builder:
|
||||||
return E.div(
|
return E.div(
|
||||||
E.a(item.label, class_="navbar-link"),
|
E.a(item.label, class_="navbar-link"),
|
||||||
E.div(
|
E.div(
|
||||||
*(_render_navbar_item(subitem, request) for subitem in item.items),
|
*(
|
||||||
|
_render_navbar_item(subitem, request)
|
||||||
|
for subitem in item.items
|
||||||
|
),
|
||||||
class_="navbar-dropdown",
|
class_="navbar-dropdown",
|
||||||
),
|
),
|
||||||
class_="navbar-item has-dropdown is-hoverable",
|
class_="navbar-item has-dropdown is-hoverable",
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from html5tagger import Builder, E # type: ignore
|
|
||||||
from sanic import Request
|
|
||||||
|
|
||||||
from webapp.display.layouts.models import MenuItem
|
from webapp.display.layouts.models import MenuItem
|
||||||
from webapp.display.text import slugify
|
from webapp.display.text import slugify
|
||||||
|
|
||||||
|
from html5tagger import Builder, E # type: ignore
|
||||||
|
from sanic import Request
|
||||||
|
|
||||||
|
|
||||||
def do_sidebar(builder: Builder, request: Request) -> None:
|
def do_sidebar(builder: Builder, request: Request) -> None:
|
||||||
builder.a(class_="burger")(E.span().span().span().span())
|
builder.a(class_="burger")(E.span().span().span().span())
|
||||||
|
@ -15,7 +15,9 @@ def _menu_items(request: Request) -> list[Builder]:
|
||||||
_sanic_logo(request),
|
_sanic_logo(request),
|
||||||
*_sidebar_items(request),
|
*_sidebar_items(request),
|
||||||
E.hr(),
|
E.hr(),
|
||||||
E.p("Current with version ").strong(request.app.config.GENERAL.current_version),
|
E.p("Current with version ").strong(
|
||||||
|
request.app.config.GENERAL.current_version
|
||||||
|
),
|
||||||
E.hr(),
|
E.hr(),
|
||||||
E.p("Want more? ").a(
|
E.p("Want more? ").a(
|
||||||
"sanicbook.com", href="https://sanicbook.com", target="_blank"
|
"sanicbook.com", href="https://sanicbook.com", target="_blank"
|
||||||
|
@ -71,7 +73,9 @@ def _single_sidebar_item(item: MenuItem, request: Request) -> Builder:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
classes: list[str] = []
|
classes: list[str] = []
|
||||||
li_classes = "menu-item"
|
li_classes = "menu-item"
|
||||||
_, page, _ = request.app.ctx.get_page(request.ctx.language, item.path or "")
|
_, page, _ = request.app.ctx.get_page(
|
||||||
|
request.ctx.language, item.path or ""
|
||||||
|
)
|
||||||
if request.path == path:
|
if request.path == path:
|
||||||
classes.append("is-active")
|
classes.append("is-active")
|
||||||
if item.href:
|
if item.href:
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from sanic import Request
|
|
||||||
|
|
||||||
from webapp.display.layouts.elements.footer import do_footer
|
from webapp.display.layouts.elements.footer import do_footer
|
||||||
from webapp.display.layouts.elements.navbar import do_navbar
|
from webapp.display.layouts.elements.navbar import do_navbar
|
||||||
from webapp.display.layouts.elements.sidebar import do_sidebar
|
from webapp.display.layouts.elements.sidebar import do_sidebar
|
||||||
|
|
||||||
|
from sanic import Request
|
||||||
|
|
||||||
from .base import BaseLayout
|
from .base import BaseLayout
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import re
|
import re
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from html5tagger import HTML, Builder, E # type: ignore
|
|
||||||
from mistune import HTMLRenderer, create_markdown, escape
|
from mistune import HTMLRenderer, create_markdown, escape
|
||||||
from mistune.directives import RSTDirective, TableOfContents
|
from mistune.directives import RSTDirective, TableOfContents
|
||||||
from mistune.util import safe_entity
|
from mistune.util import safe_entity
|
||||||
|
@ -9,6 +8,8 @@ from pygments import highlight
|
||||||
from pygments.formatters import html
|
from pygments.formatters import html
|
||||||
from pygments.lexers import get_lexer_by_name
|
from pygments.lexers import get_lexer_by_name
|
||||||
|
|
||||||
|
from html5tagger import HTML, Builder, E # type: ignore
|
||||||
|
|
||||||
from .code_style import SanicCodeStyle
|
from .code_style import SanicCodeStyle
|
||||||
from .plugins.attrs import Attributes
|
from .plugins.attrs import Attributes
|
||||||
from .plugins.columns import Column
|
from .plugins.columns import Column
|
||||||
|
@ -36,9 +37,9 @@ class DocsRenderer(HTMLRenderer):
|
||||||
class_="code-block__copy",
|
class_="code-block__copy",
|
||||||
onclick="copyCode(this)",
|
onclick="copyCode(this)",
|
||||||
):
|
):
|
||||||
builder.div(class_="code-block__rectangle code-block__filled").div(
|
builder.div(
|
||||||
class_="code-block__rectangle code-block__outlined"
|
class_="code-block__rectangle code-block__filled"
|
||||||
)
|
).div(class_="code-block__rectangle code-block__outlined")
|
||||||
else:
|
else:
|
||||||
builder.pre(E.code(escape(code)))
|
builder.pre(E.code(escape(code)))
|
||||||
return str(builder)
|
return str(builder)
|
||||||
|
@ -46,7 +47,9 @@ class DocsRenderer(HTMLRenderer):
|
||||||
def heading(self, text: str, level: int, **attrs) -> str:
|
def heading(self, text: str, level: int, **attrs) -> str:
|
||||||
ident = slugify(text)
|
ident = slugify(text)
|
||||||
if level > 1:
|
if level > 1:
|
||||||
text += self._make_tag("a", {"href": f"#{ident}", "class": "anchor"}, "#")
|
text += self._make_tag(
|
||||||
|
"a", {"href": f"#{ident}", "class": "anchor"}, "#"
|
||||||
|
)
|
||||||
return self._make_tag(
|
return self._make_tag(
|
||||||
f"h{level}", {"id": ident, "class": f"is-size-{level}"}, text
|
f"h{level}", {"id": ident, "class": f"is-size-{level}"}, text
|
||||||
)
|
)
|
||||||
|
@ -90,7 +93,9 @@ class DocsRenderer(HTMLRenderer):
|
||||||
def _make_tag(
|
def _make_tag(
|
||||||
self, tag: str, attributes: dict[str, str], text: str | None = None
|
self, tag: str, attributes: dict[str, str], text: str | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
attrs = " ".join(f'{key}="{value}"' for key, value in attributes.items())
|
attrs = " ".join(
|
||||||
|
f'{key}="{value}"' for key, value in attributes.items()
|
||||||
|
)
|
||||||
if text is None:
|
if text is None:
|
||||||
return f"<{tag} {attrs} />"
|
return f"<{tag} {attrs} />"
|
||||||
return f"<{tag} {attrs}>{text}</{tag}>"
|
return f"<{tag} {attrs}>{text}</{tag}>"
|
||||||
|
|
|
@ -10,6 +10,7 @@ from html import escape
|
||||||
from docstring_parser import Docstring, DocstringParam, DocstringRaises
|
from docstring_parser import Docstring, DocstringParam, DocstringRaises
|
||||||
from docstring_parser import parse as parse_docstring
|
from docstring_parser import parse as parse_docstring
|
||||||
from docstring_parser.common import DocstringExample
|
from docstring_parser.common import DocstringExample
|
||||||
|
|
||||||
from html5tagger import HTML, Builder, E # type: ignore
|
from html5tagger import HTML, Builder, E # type: ignore
|
||||||
|
|
||||||
from ..markdown import render_markdown, slugify
|
from ..markdown import render_markdown, slugify
|
||||||
|
@ -119,7 +120,9 @@ def _extract_docobjects(package_name: str) -> dict[str, DocObject]:
|
||||||
docstrings = {}
|
docstrings = {}
|
||||||
package = importlib.import_module(package_name)
|
package = importlib.import_module(package_name)
|
||||||
|
|
||||||
for _, name, _ in pkgutil.walk_packages(package.__path__, package_name + "."):
|
for _, name, _ in pkgutil.walk_packages(
|
||||||
|
package.__path__, package_name + "."
|
||||||
|
):
|
||||||
module = importlib.import_module(name)
|
module = importlib.import_module(name)
|
||||||
for obj_name, obj in inspect.getmembers(module):
|
for obj_name, obj in inspect.getmembers(module):
|
||||||
if (
|
if (
|
||||||
|
@ -153,7 +156,9 @@ def _docobject_to_html(
|
||||||
) -> None:
|
) -> None:
|
||||||
anchor_id = slugify(docobject.full_name.replace(".", "-"))
|
anchor_id = slugify(docobject.full_name.replace(".", "-"))
|
||||||
anchor = E.a("#", class_="anchor", href=f"#{anchor_id}")
|
anchor = E.a("#", class_="anchor", href=f"#{anchor_id}")
|
||||||
class_name, heading = _define_heading_and_class(docobject, anchor, as_method)
|
class_name, heading = _define_heading_and_class(
|
||||||
|
docobject, anchor, as_method
|
||||||
|
)
|
||||||
|
|
||||||
with builder.div(class_=class_name):
|
with builder.div(class_=class_name):
|
||||||
builder(heading)
|
builder(heading)
|
||||||
|
@ -207,7 +212,9 @@ def _docobject_to_html(
|
||||||
|
|
||||||
if docobject.docstring.params:
|
if docobject.docstring.params:
|
||||||
with builder.div(class_="box mt-5"):
|
with builder.div(class_="box mt-5"):
|
||||||
builder.h5("Parameters", class_="is-size-5 has-text-weight-bold")
|
builder.h5(
|
||||||
|
"Parameters", class_="is-size-5 has-text-weight-bold"
|
||||||
|
)
|
||||||
_render_params(builder, docobject.docstring.params)
|
_render_params(builder, docobject.docstring.params)
|
||||||
|
|
||||||
if docobject.docstring.returns:
|
if docobject.docstring.returns:
|
||||||
|
@ -232,7 +239,9 @@ def _signature_to_html(
|
||||||
parts = []
|
parts = []
|
||||||
parts.append("<span class='function-signature'>")
|
parts.append("<span class='function-signature'>")
|
||||||
for decorator in decorators:
|
for decorator in decorators:
|
||||||
parts.append(f"<span class='function-decorator'>@{decorator}</span><br>")
|
parts.append(
|
||||||
|
f"<span class='function-decorator'>@{decorator}</span><br>"
|
||||||
|
)
|
||||||
parts.append(
|
parts.append(
|
||||||
f"<span class='is-italic'>{object_type}</span> "
|
f"<span class='is-italic'>{object_type}</span> "
|
||||||
f"<span class='has-text-weight-bold'>{name}</span>("
|
f"<span class='has-text-weight-bold'>{name}</span>("
|
||||||
|
@ -246,7 +255,9 @@ def _signature_to_html(
|
||||||
annotation = ""
|
annotation = ""
|
||||||
if param.annotation != inspect.Parameter.empty:
|
if param.annotation != inspect.Parameter.empty:
|
||||||
annotation = escape(str(param.annotation))
|
annotation = escape(str(param.annotation))
|
||||||
parts.append(f": <span class='param-annotation'>{annotation}</span>")
|
parts.append(
|
||||||
|
f": <span class='param-annotation'>{annotation}</span>"
|
||||||
|
)
|
||||||
if param.default != inspect.Parameter.empty:
|
if param.default != inspect.Parameter.empty:
|
||||||
default = escape(str(param.default))
|
default = escape(str(param.default))
|
||||||
if annotation == "str":
|
if annotation == "str":
|
||||||
|
@ -257,7 +268,9 @@ def _signature_to_html(
|
||||||
parts.append(")")
|
parts.append(")")
|
||||||
if signature.return_annotation != inspect.Signature.empty:
|
if signature.return_annotation != inspect.Signature.empty:
|
||||||
return_annotation = escape(str(signature.return_annotation))
|
return_annotation = escape(str(signature.return_annotation))
|
||||||
parts.append(f": -> <span class='return-annotation'>{return_annotation}</span>")
|
parts.append(
|
||||||
|
f": -> <span class='return-annotation'>{return_annotation}</span>"
|
||||||
|
)
|
||||||
parts.append("</span>")
|
parts.append("</span>")
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|
||||||
|
@ -305,7 +318,10 @@ def _render_params(builder: Builder, params: list[DocstringParam]) -> None:
|
||||||
builder.dd(
|
builder.dd(
|
||||||
HTML(
|
HTML(
|
||||||
render_markdown(
|
render_markdown(
|
||||||
param.description or param.arg_name or param.type_name or ""
|
param.description
|
||||||
|
or param.arg_name
|
||||||
|
or param.type_name
|
||||||
|
or ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -318,7 +334,11 @@ def _render_raises(builder: Builder, raises: list[DocstringRaises]) -> None:
|
||||||
with builder.dl(class_="mt-2"):
|
with builder.dl(class_="mt-2"):
|
||||||
builder.dt(raise_.type_name, class_="is-family-monospace")
|
builder.dt(raise_.type_name, class_="is-family-monospace")
|
||||||
builder.dd(
|
builder.dd(
|
||||||
HTML(render_markdown(raise_.description or raise_.type_name or ""))
|
HTML(
|
||||||
|
render_markdown(
|
||||||
|
raise_.description or raise_.type_name or ""
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -334,7 +354,11 @@ def _render_returns(builder: Builder, docobject: DocObject) -> None:
|
||||||
if not return_type or return_type == inspect.Signature.empty:
|
if not return_type or return_type == inspect.Signature.empty:
|
||||||
return_type = "N/A"
|
return_type = "N/A"
|
||||||
|
|
||||||
term = "Return" if not docobject.docstring.returns.is_generator else "Yields"
|
term = (
|
||||||
|
"Return"
|
||||||
|
if not docobject.docstring.returns.is_generator
|
||||||
|
else "Yields"
|
||||||
|
)
|
||||||
builder.h5(term, class_="is-size-5 has-text-weight-bold")
|
builder.h5(term, class_="is-size-5 has-text-weight-bold")
|
||||||
with builder.dl(class_="mt-2"):
|
with builder.dl(class_="mt-2"):
|
||||||
builder.dt(return_type, class_="is-family-monospace")
|
builder.dt(return_type, class_="is-family-monospace")
|
||||||
|
@ -349,11 +373,17 @@ def _render_returns(builder: Builder, docobject: DocObject) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _render_examples(builder: Builder, examples: list[DocstringExample]) -> None:
|
def _render_examples(
|
||||||
|
builder: Builder, examples: list[DocstringExample]
|
||||||
|
) -> None:
|
||||||
with builder.div(class_="box mt-5"):
|
with builder.div(class_="box mt-5"):
|
||||||
builder.h5("Examples", class_="is-size-5 has-text-weight-bold")
|
builder.h5("Examples", class_="is-size-5 has-text-weight-bold")
|
||||||
for example in examples:
|
for example in examples:
|
||||||
with builder.div(class_="mt-2"):
|
with builder.div(class_="mt-2"):
|
||||||
builder(
|
builder(
|
||||||
HTML(render_markdown(example.description or example.snippet or ""))
|
HTML(
|
||||||
|
render_markdown(
|
||||||
|
example.description or example.snippet or ""
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from frontmatter import parse
|
from frontmatter import parse
|
||||||
|
|
||||||
|
@ -11,8 +12,10 @@ from ..layouts.main import MainLayout
|
||||||
from ..markdown import render_markdown
|
from ..markdown import render_markdown
|
||||||
from .docobject import organize_docobjects
|
from .docobject import organize_docobjects
|
||||||
|
|
||||||
_PAGE_CACHE: dict[str, dict[str, tuple[Page | None, Page | None, Page | None]]] = {}
|
_PAGE_CACHE: dict[
|
||||||
_LAYOUTS_CACHE: dict[str, type[BaseLayout]] = {
|
str, dict[str, tuple[Page | None, Page | None, Page | None]]
|
||||||
|
] = {}
|
||||||
|
_LAYOUTS_CACHE: dict[str, Type[BaseLayout]] = {
|
||||||
"home": HomeLayout,
|
"home": HomeLayout,
|
||||||
"main": MainLayout,
|
"main": MainLayout,
|
||||||
}
|
}
|
||||||
|
@ -40,7 +43,7 @@ class Page:
|
||||||
|
|
||||||
DEFAULT_LANGUAGE = _DEFAULT
|
DEFAULT_LANGUAGE = _DEFAULT
|
||||||
|
|
||||||
def get_layout(self) -> type[BaseLayout]:
|
def get_layout(self) -> Type[BaseLayout]:
|
||||||
return _LAYOUTS_CACHE[self.meta.layout]
|
return _LAYOUTS_CACHE[self.meta.layout]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from webapp.display.base import BaseRenderer
|
||||||
|
|
||||||
from html5tagger import HTML, Builder # type: ignore
|
from html5tagger import HTML, Builder # type: ignore
|
||||||
from sanic import Request
|
from sanic import Request
|
||||||
|
|
||||||
from webapp.display.base import BaseRenderer
|
|
||||||
|
|
||||||
from ..layouts.base import BaseLayout
|
from ..layouts.base import BaseLayout
|
||||||
from .page import Page
|
from .page import Page
|
||||||
|
|
||||||
|
@ -20,9 +21,13 @@ class PageRenderer(BaseRenderer):
|
||||||
self._body(request, builder, language, path)
|
self._body(request, builder, language, path)
|
||||||
return builder
|
return builder
|
||||||
|
|
||||||
def _body(self, request: Request, builder: Builder, language: str, path: str):
|
def _body(
|
||||||
|
self, request: Request, builder: Builder, language: str, path: str
|
||||||
|
):
|
||||||
prev_page, current_page, next_page = Page.get(language, path)
|
prev_page, current_page, next_page = Page.get(language, path)
|
||||||
request.ctx.language = Page.DEFAULT_LANGUAGE if language == "api" else language
|
request.ctx.language = (
|
||||||
|
Page.DEFAULT_LANGUAGE if language == "api" else language
|
||||||
|
)
|
||||||
request.ctx.current_page = current_page
|
request.ctx.current_page = current_page
|
||||||
request.ctx.previous_page = prev_page
|
request.ctx.previous_page = prev_page
|
||||||
request.ctx.next_page = next_page
|
request.ctx.next_page = next_page
|
||||||
|
@ -34,7 +39,9 @@ class PageRenderer(BaseRenderer):
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _base(self, request: Request, builder: Builder, page: Page | None):
|
def _base(self, request: Request, builder: Builder, page: Page | None):
|
||||||
layout_type: type[BaseLayout] = page.get_layout() if page else BaseLayout
|
layout_type: Type[BaseLayout] = (
|
||||||
|
page.get_layout() if page else BaseLayout
|
||||||
|
)
|
||||||
layout = layout_type(builder)
|
layout = layout_type(builder)
|
||||||
with layout(request, builder.full):
|
with layout(request, builder.full):
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -2,11 +2,12 @@ from re import Match
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from html5tagger import HTML, E
|
|
||||||
from mistune.block_parser import BlockParser
|
from mistune.block_parser import BlockParser
|
||||||
from mistune.core import BlockState
|
from mistune.core import BlockState
|
||||||
from mistune.directives import DirectivePlugin
|
from mistune.directives import DirectivePlugin
|
||||||
|
|
||||||
|
from html5tagger import HTML, E
|
||||||
|
|
||||||
|
|
||||||
class Attributes(DirectivePlugin):
|
class Attributes(DirectivePlugin):
|
||||||
def __call__(self, directive, md):
|
def __call__(self, directive, md):
|
||||||
|
@ -15,7 +16,9 @@ class Attributes(DirectivePlugin):
|
||||||
if md.renderer.NAME == "html":
|
if md.renderer.NAME == "html":
|
||||||
md.renderer.register("attrs", self._render)
|
md.renderer.register("attrs", self._render)
|
||||||
|
|
||||||
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]:
|
def parse(
|
||||||
|
self, block: BlockParser, m: Match, state: BlockState
|
||||||
|
) -> dict[str, Any]:
|
||||||
info = m.groupdict()
|
info = m.groupdict()
|
||||||
options = dict(self.parse_options(m))
|
options = dict(self.parse_options(m))
|
||||||
new_state = block.state_cls()
|
new_state = block.state_cls()
|
||||||
|
|
|
@ -10,7 +10,9 @@ from mistune.markdown import Markdown
|
||||||
|
|
||||||
|
|
||||||
class Column(DirectivePlugin):
|
class Column(DirectivePlugin):
|
||||||
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]:
|
def parse(
|
||||||
|
self, block: BlockParser, m: Match, state: BlockState
|
||||||
|
) -> dict[str, Any]:
|
||||||
info = m.groupdict()
|
info = m.groupdict()
|
||||||
|
|
||||||
new_state = block.state_cls()
|
new_state = block.state_cls()
|
||||||
|
@ -34,7 +36,9 @@ class Column(DirectivePlugin):
|
||||||
|
|
||||||
def _render_column(self, renderer: HTMLRenderer, text: str, **attrs):
|
def _render_column(self, renderer: HTMLRenderer, text: str, **attrs):
|
||||||
start = (
|
start = (
|
||||||
'<div class="columns mt-3 is-multiline">\n' if attrs.get("first") else ""
|
'<div class="columns mt-3 is-multiline">\n'
|
||||||
|
if attrs.get("first")
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
end = "</div>\n" if attrs.get("last") else ""
|
end = "</div>\n" if attrs.get("last") else ""
|
||||||
col = f'<div class="column is-half">{text}</div>\n'
|
col = f'<div class="column is-half">{text}</div>\n'
|
||||||
|
|
|
@ -16,12 +16,16 @@ class Hook(DirectivePlugin):
|
||||||
for type_ in ("column", "tab"):
|
for type_ in ("column", "tab"):
|
||||||
if token["type"] == type_:
|
if token["type"] == type_:
|
||||||
maybe_next = (
|
maybe_next = (
|
||||||
state.tokens[idx + 1] if idx + 1 < len(state.tokens) else None
|
state.tokens[idx + 1]
|
||||||
|
if idx + 1 < len(state.tokens)
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
token.setdefault("attrs", {})
|
token.setdefault("attrs", {})
|
||||||
if prev and prev["type"] != type_:
|
if prev and prev["type"] != type_:
|
||||||
token["attrs"]["first"] = True
|
token["attrs"]["first"] = True
|
||||||
if (maybe_next and maybe_next["type"] != type_) or not maybe_next:
|
if (
|
||||||
|
maybe_next and maybe_next["type"] != type_
|
||||||
|
) or not maybe_next:
|
||||||
token["attrs"]["last"] = True
|
token["attrs"]["last"] = True
|
||||||
|
|
||||||
prev = token
|
prev = token
|
||||||
|
|
|
@ -3,16 +3,19 @@ from re import Match
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from html5tagger import HTML, E
|
|
||||||
from mistune import HTMLRenderer
|
from mistune import HTMLRenderer
|
||||||
from mistune.block_parser import BlockParser
|
from mistune.block_parser import BlockParser
|
||||||
from mistune.core import BlockState
|
from mistune.core import BlockState
|
||||||
from mistune.directives import DirectivePlugin, RSTDirective
|
from mistune.directives import DirectivePlugin, RSTDirective
|
||||||
from mistune.markdown import Markdown
|
from mistune.markdown import Markdown
|
||||||
|
|
||||||
|
from html5tagger import HTML, E
|
||||||
|
|
||||||
|
|
||||||
class Mermaid(DirectivePlugin):
|
class Mermaid(DirectivePlugin):
|
||||||
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]:
|
def parse(
|
||||||
|
self, block: BlockParser, m: Match, state: BlockState
|
||||||
|
) -> dict[str, Any]:
|
||||||
info = m.groupdict()
|
info = m.groupdict()
|
||||||
|
|
||||||
new_state = block.state_cls()
|
new_state = block.state_cls()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from html5tagger import HTML, E
|
|
||||||
from mistune.directives import Admonition
|
from mistune.directives import Admonition
|
||||||
|
|
||||||
|
from html5tagger import HTML, E
|
||||||
|
|
||||||
|
|
||||||
class Notification(Admonition):
|
class Notification(Admonition):
|
||||||
SUPPORTED_NAMES = {
|
SUPPORTED_NAMES = {
|
||||||
|
@ -19,8 +20,12 @@ class Notification(Admonition):
|
||||||
|
|
||||||
if md.renderer.NAME == "html":
|
if md.renderer.NAME == "html":
|
||||||
md.renderer.register("admonition", self._render_admonition)
|
md.renderer.register("admonition", self._render_admonition)
|
||||||
md.renderer.register("admonition_title", self._render_admonition_title)
|
md.renderer.register(
|
||||||
md.renderer.register("admonition_content", self._render_admonition_content)
|
"admonition_title", self._render_admonition_title
|
||||||
|
)
|
||||||
|
md.renderer.register(
|
||||||
|
"admonition_content", self._render_admonition_content
|
||||||
|
)
|
||||||
|
|
||||||
def _render_admonition(self, _, text, name, **attrs) -> str:
|
def _render_admonition(self, _, text, name, **attrs) -> str:
|
||||||
return str(
|
return str(
|
||||||
|
|
|
@ -10,7 +10,9 @@ from mistune.markdown import Markdown
|
||||||
|
|
||||||
|
|
||||||
class Tabs(DirectivePlugin):
|
class Tabs(DirectivePlugin):
|
||||||
def parse(self, block: BlockParser, m: Match, state: BlockState) -> dict[str, Any]:
|
def parse(
|
||||||
|
self, block: BlockParser, m: Match, state: BlockState
|
||||||
|
) -> dict[str, Any]:
|
||||||
info = m.groupdict()
|
info = m.groupdict()
|
||||||
|
|
||||||
new_state = block.state_cls()
|
new_state = block.state_cls()
|
||||||
|
@ -39,7 +41,9 @@ class Tabs(DirectivePlugin):
|
||||||
def _render_tab(self, renderer: HTMLRenderer, text: str, **attrs):
|
def _render_tab(self, renderer: HTMLRenderer, text: str, **attrs):
|
||||||
start = '<div class="tabs mt-6"><ul>\n' if attrs.get("first") else ""
|
start = '<div class="tabs mt-6"><ul>\n' if attrs.get("first") else ""
|
||||||
end = (
|
end = (
|
||||||
'</ul></div><div class="tab-display"></div>\n' if attrs.get("last") else ""
|
'</ul></div><div class="tab-display"></div>\n'
|
||||||
|
if attrs.get("last")
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
content = f'<div class="tab-content">{text}</div>\n'
|
content = f'<div class="tab-content">{text}</div>\n'
|
||||||
tab = f'<li><a>{attrs["title"]}</a>{content}</li>\n'
|
tab = f'<li><a>{attrs["title"]}</a>{content}</li>\n'
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from webapp.display.search.search import Searcher
|
||||||
|
|
||||||
from html5tagger import Builder, E # type: ignore
|
from html5tagger import Builder, E # type: ignore
|
||||||
from sanic import Request
|
from sanic import Request
|
||||||
|
|
||||||
from webapp.display.search.search import Searcher
|
|
||||||
|
|
||||||
from ..base import BaseRenderer
|
from ..base import BaseRenderer
|
||||||
from ..layouts.main import MainLayout
|
from ..layouts.main import MainLayout
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ from pathlib import Path
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from msgspec import Struct
|
from msgspec import Struct
|
||||||
|
|
||||||
from webapp.display.page import Page
|
from webapp.display.page import Page
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,7 +91,9 @@ def _inverse_document_frequency(docs: list[Document]) -> dict[str, float]:
|
||||||
return {word: num_docs / count for word, count in word_count.items()}
|
return {word: num_docs / count for word, count in word_count.items()}
|
||||||
|
|
||||||
|
|
||||||
def _tf_idf_vector(document: Document, idf: dict[str, float]) -> dict[str, float]:
|
def _tf_idf_vector(
|
||||||
|
document: Document, idf: dict[str, float]
|
||||||
|
) -> dict[str, float]:
|
||||||
"""Calculate the TF-IDF vector for a document."""
|
"""Calculate the TF-IDF vector for a document."""
|
||||||
return {
|
return {
|
||||||
word: tf * idf[word]
|
word: tf * idf[word]
|
||||||
|
@ -101,7 +102,9 @@ def _tf_idf_vector(document: Document, idf: dict[str, float]) -> dict[str, float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _cosine_similarity(vec1: dict[str, float], vec2: dict[str, float]) -> float:
|
def _cosine_similarity(
|
||||||
|
vec1: dict[str, float], vec2: dict[str, float]
|
||||||
|
) -> float:
|
||||||
"""Calculate the cosine similarity between two vectors."""
|
"""Calculate the cosine similarity between two vectors."""
|
||||||
if not vec1 or not vec2:
|
if not vec1 or not vec2:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
@ -123,7 +126,9 @@ def _search(
|
||||||
tf_idf_query = _tf_idf_vector(
|
tf_idf_query = _tf_idf_vector(
|
||||||
Document(page=dummy_page, language=language).process(stemmer), idf
|
Document(page=dummy_page, language=language).process(stemmer), idf
|
||||||
)
|
)
|
||||||
similarities = [_cosine_similarity(tf_idf_query, vector) for vector in vectors]
|
similarities = [
|
||||||
|
_cosine_similarity(tf_idf_query, vector) for vector in vectors
|
||||||
|
]
|
||||||
return [
|
return [
|
||||||
(similarity, document)
|
(similarity, document)
|
||||||
for similarity, document in sorted(
|
for similarity, document in sorted(
|
||||||
|
@ -150,13 +155,16 @@ class Searcher:
|
||||||
}
|
}
|
||||||
self._vectors = {
|
self._vectors = {
|
||||||
language: [
|
language: [
|
||||||
_tf_idf_vector(document, self._idf[language]) for document in documents
|
_tf_idf_vector(document, self._idf[language])
|
||||||
|
for document in documents
|
||||||
]
|
]
|
||||||
for language, documents in self._documents.items()
|
for language, documents in self._documents.items()
|
||||||
}
|
}
|
||||||
self._stemmer = stemmer
|
self._stemmer = stemmer
|
||||||
|
|
||||||
def search(self, query: str, language: str) -> list[tuple[float, Document]]:
|
def search(
|
||||||
|
self, query: str, language: str
|
||||||
|
) -> list[tuple[float, Document]]:
|
||||||
return _search(
|
return _search(
|
||||||
query,
|
query,
|
||||||
language,
|
language,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# from urllib.parse import unquote
|
# from urllib.parse import unquote
|
||||||
|
|
||||||
from sanic import Blueprint, Request, Sanic, html
|
|
||||||
|
|
||||||
from webapp.display.page import Page
|
from webapp.display.page import Page
|
||||||
from webapp.display.search.renderer import SearchRenderer
|
from webapp.display.search.renderer import SearchRenderer
|
||||||
from webapp.display.search.search import Document, Searcher, Stemmer
|
from webapp.display.search.search import Document, Searcher, Stemmer
|
||||||
|
|
||||||
|
from sanic import Blueprint, Request, Sanic, html
|
||||||
|
|
||||||
bp = Blueprint("search", url_prefix="/<language>/search")
|
bp = Blueprint("search", url_prefix="/<language>/search")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from msgspec import yaml
|
from msgspec import yaml
|
||||||
|
|
||||||
from webapp.display.layouts.models import GeneralConfig, MenuItem
|
from webapp.display.layouts.models import GeneralConfig, MenuItem
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sanic import Request, Sanic, html, redirect
|
|
||||||
|
|
||||||
from webapp.display.layouts.models import MenuItem
|
from webapp.display.layouts.models import MenuItem
|
||||||
from webapp.display.page import Page, PageRenderer
|
from webapp.display.page import Page, PageRenderer
|
||||||
from webapp.endpoint.view import bp
|
from webapp.endpoint.view import bp
|
||||||
|
@ -9,6 +7,8 @@ from webapp.worker.config import load_config, load_menu
|
||||||
from webapp.worker.reload import setup_livereload
|
from webapp.worker.reload import setup_livereload
|
||||||
from webapp.worker.style import setup_style
|
from webapp.worker.style import setup_style
|
||||||
|
|
||||||
|
from sanic import Request, Sanic, html, redirect
|
||||||
|
|
||||||
|
|
||||||
def _compile_sidebar_order(items: list[MenuItem]) -> list[str]:
|
def _compile_sidebar_order(items: list[MenuItem]) -> list[str]:
|
||||||
order = []
|
order = []
|
||||||
|
@ -28,9 +28,13 @@ def create_app(root: Path) -> Sanic:
|
||||||
app.config.STYLE_DIR = root / "style"
|
app.config.STYLE_DIR = root / "style"
|
||||||
app.config.NODE_MODULES_DIR = root / "node_modules"
|
app.config.NODE_MODULES_DIR = root / "node_modules"
|
||||||
app.config.LANGUAGES = ["en"]
|
app.config.LANGUAGES = ["en"]
|
||||||
app.config.SIDEBAR = load_menu(app.config.CONFIG_DIR / "en" / "sidebar.yaml")
|
app.config.SIDEBAR = load_menu(
|
||||||
|
app.config.CONFIG_DIR / "en" / "sidebar.yaml"
|
||||||
|
)
|
||||||
app.config.NAVBAR = load_menu(app.config.CONFIG_DIR / "en" / "navbar.yaml")
|
app.config.NAVBAR = load_menu(app.config.CONFIG_DIR / "en" / "navbar.yaml")
|
||||||
app.config.GENERAL = load_config(app.config.CONFIG_DIR / "en" / "general.yaml")
|
app.config.GENERAL = load_config(
|
||||||
|
app.config.CONFIG_DIR / "en" / "general.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
setup_livereload(app)
|
setup_livereload(app)
|
||||||
setup_style(app)
|
setup_style(app)
|
||||||
|
@ -62,6 +66,8 @@ def create_app(root: Path) -> Sanic:
|
||||||
|
|
||||||
@app.on_request
|
@app.on_request
|
||||||
async def set_language(request: Request):
|
async def set_language(request: Request):
|
||||||
request.ctx.language = request.match_info.get("language", Page.DEFAULT_LANGUAGE)
|
request.ctx.language = request.match_info.get(
|
||||||
|
"language", Page.DEFAULT_LANGUAGE
|
||||||
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -5,6 +5,7 @@ from queue import Empty, Queue
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
from sanic import Request, Sanic, Websocket
|
from sanic import Request, Sanic, Websocket
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,12 +54,16 @@ class Livereload:
|
||||||
"serverName": SERVER_NAME,
|
"serverName": SERVER_NAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, reload_queue: Queue, debug: bool, state: dict[str, Any]):
|
def __init__(
|
||||||
|
self, reload_queue: Queue, debug: bool, state: dict[str, Any]
|
||||||
|
):
|
||||||
self.reload_queue = reload_queue
|
self.reload_queue = reload_queue
|
||||||
self.app = Sanic(self.SERVER_NAME)
|
self.app = Sanic(self.SERVER_NAME)
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.state = state
|
self.state = state
|
||||||
self.app.static("/livereload.js", Path(__file__).parent / "livereload.js")
|
self.app.static(
|
||||||
|
"/livereload.js", Path(__file__).parent / "livereload.js"
|
||||||
|
)
|
||||||
self.app.add_websocket_route(
|
self.app.add_websocket_route(
|
||||||
self.livereload_handler, "/livereload", name="livereload"
|
self.livereload_handler, "/livereload", name="livereload"
|
||||||
)
|
)
|
||||||
|
@ -104,5 +109,7 @@ class Livereload:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def _run_reload_server(reload_queue: Queue, debug: bool, state: dict[str, Any]):
|
def _run_reload_server(
|
||||||
|
reload_queue: Queue, debug: bool, state: dict[str, Any]
|
||||||
|
):
|
||||||
Livereload(reload_queue, debug, state).run()
|
Livereload(reload_queue, debug, state).run()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# from scss.compiler import compile_string
|
# from scss.compiler import compile_string
|
||||||
|
|
||||||
from pygments.formatters import html
|
from pygments.formatters import html
|
||||||
from sanic import Sanic
|
|
||||||
from sass import compile as compile_scss
|
from sass import compile as compile_scss
|
||||||
|
|
||||||
from webapp.display.code_style import SanicCodeStyle
|
from webapp.display.code_style import SanicCodeStyle
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
def setup_style(app: Sanic) -> None:
|
def setup_style(app: Sanic) -> None:
|
||||||
index = app.config.STYLE_DIR / "index.scss"
|
index = app.config.STYLE_DIR / "index.scss"
|
||||||
|
|
|
@ -2,28 +2,20 @@
|
||||||
requires = ["setuptools", "wheel"]
|
requires = ["setuptools", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.black]
|
||||||
extend-select = ["I", "W", "UP", "C4", "ISC"]
|
line-length = 79
|
||||||
# Worth selecting but still too broken: ASYNC, S, B, DTZ, FA
|
|
||||||
ignore = [
|
|
||||||
"D100",
|
|
||||||
"D101",
|
|
||||||
"D102",
|
|
||||||
"D103",
|
|
||||||
"E402",
|
|
||||||
"E741",
|
|
||||||
"F811",
|
|
||||||
"F821",
|
|
||||||
# ruff format complains about these:
|
|
||||||
"ISC001",
|
|
||||||
"W191",
|
|
||||||
]
|
|
||||||
show-source = true
|
|
||||||
show-fixes = true
|
|
||||||
|
|
||||||
[tool.ruff.isort]
|
[tool.isort]
|
||||||
known-first-party = ["sanic"]
|
atomic = true
|
||||||
known-third-party = ["pytest"]
|
default_section = "THIRDPARTY"
|
||||||
|
include_trailing_comma = true
|
||||||
|
known_first_party = "sanic"
|
||||||
|
known_third_party = "pytest"
|
||||||
|
line_length = 79
|
||||||
|
lines_after_imports = 2
|
||||||
|
lines_between_types = 1
|
||||||
|
multi_line_output = 3
|
||||||
|
profile = "black"
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = [
|
module = [
|
||||||
|
|
|
@ -36,6 +36,7 @@ from sanic.response import (
|
||||||
)
|
)
|
||||||
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
||||||
|
|
||||||
|
|
||||||
DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]"
|
DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]"
|
||||||
"""
|
"""
|
||||||
A type alias for a Sanic app with a default config and namespace.
|
A type alias for a Sanic app with a default config and namespace.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from sanic.cli.app import SanicCLI
|
from sanic.cli.app import SanicCLI
|
||||||
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||||
|
|
||||||
|
|
||||||
if OS_IS_WINDOWS:
|
if OS_IS_WINDOWS:
|
||||||
enable_windows_color_support()
|
enable_windows_color_support()
|
||||||
|
|
||||||
|
|
375
sanic/app.py
375
sanic/app.py
|
@ -5,6 +5,7 @@ import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from asyncio import (
|
from asyncio import (
|
||||||
AbstractEventLoop,
|
AbstractEventLoop,
|
||||||
CancelledError,
|
CancelledError,
|
||||||
|
@ -31,12 +32,19 @@ from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Coroutine,
|
Coroutine,
|
||||||
|
Deque,
|
||||||
|
Dict,
|
||||||
Generic,
|
Generic,
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
|
List,
|
||||||
Literal,
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
|
Union,
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
|
@ -88,6 +96,7 @@ from sanic.worker.inspector import Inspector
|
||||||
from sanic.worker.loader import CertLoader
|
from sanic.worker.loader import CertLoader
|
||||||
from sanic.worker.manager import WorkerManager
|
from sanic.worker.manager import WorkerManager
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
try:
|
try:
|
||||||
from sanic_ext import Extend # type: ignore
|
from sanic_ext import Extend # type: ignore
|
||||||
|
@ -164,7 +173,7 @@ class Sanic(
|
||||||
"websocket_tasks",
|
"websocket_tasks",
|
||||||
)
|
)
|
||||||
|
|
||||||
_app_registry: ClassVar[dict[str, Sanic]] = {}
|
_app_registry: ClassVar[Dict[str, "Sanic"]] = {}
|
||||||
test_mode: ClassVar[bool] = False
|
test_mode: ClassVar[bool] = False
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -173,19 +182,19 @@ class Sanic(
|
||||||
name: str,
|
name: str,
|
||||||
config: None = None,
|
config: None = None,
|
||||||
ctx: None = None,
|
ctx: None = None,
|
||||||
router: Router | None = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: SignalRouter | None = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: ErrorHandler | None = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
env_prefix: str | None = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
request_class: type[Request] | None = None,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
log_config: dict[str, Any] | None = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
dumps: Callable[..., AnyStr] | None = None,
|
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||||
loads: Callable[..., Any] | None = None,
|
loads: Optional[Callable[..., Any]] = None,
|
||||||
inspector: bool = False,
|
inspector: bool = False,
|
||||||
inspector_class: type[Inspector] | None = None,
|
inspector_class: Optional[Type[Inspector]] = None,
|
||||||
certloader_class: type[CertLoader] | None = None,
|
certloader_class: Optional[Type[CertLoader]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -193,21 +202,21 @@ class Sanic(
|
||||||
def __init__(
|
def __init__(
|
||||||
self: Sanic[config_type, SimpleNamespace],
|
self: Sanic[config_type, SimpleNamespace],
|
||||||
name: str,
|
name: str,
|
||||||
config: config_type | None = None,
|
config: Optional[config_type] = None,
|
||||||
ctx: None = None,
|
ctx: None = None,
|
||||||
router: Router | None = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: SignalRouter | None = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: ErrorHandler | None = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
env_prefix: str | None = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
request_class: type[Request] | None = None,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
log_config: dict[str, Any] | None = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
dumps: Callable[..., AnyStr] | None = None,
|
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||||
loads: Callable[..., Any] | None = None,
|
loads: Optional[Callable[..., Any]] = None,
|
||||||
inspector: bool = False,
|
inspector: bool = False,
|
||||||
inspector_class: type[Inspector] | None = None,
|
inspector_class: Optional[Type[Inspector]] = None,
|
||||||
certloader_class: type[CertLoader] | None = None,
|
certloader_class: Optional[Type[CertLoader]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -216,20 +225,20 @@ class Sanic(
|
||||||
self: Sanic[Config, ctx_type],
|
self: Sanic[Config, ctx_type],
|
||||||
name: str,
|
name: str,
|
||||||
config: None = None,
|
config: None = None,
|
||||||
ctx: ctx_type | None = None,
|
ctx: Optional[ctx_type] = None,
|
||||||
router: Router | None = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: SignalRouter | None = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: ErrorHandler | None = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
env_prefix: str | None = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
request_class: type[Request] | None = None,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
log_config: dict[str, Any] | None = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
dumps: Callable[..., AnyStr] | None = None,
|
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||||
loads: Callable[..., Any] | None = None,
|
loads: Optional[Callable[..., Any]] = None,
|
||||||
inspector: bool = False,
|
inspector: bool = False,
|
||||||
inspector_class: type[Inspector] | None = None,
|
inspector_class: Optional[Type[Inspector]] = None,
|
||||||
certloader_class: type[CertLoader] | None = None,
|
certloader_class: Optional[Type[CertLoader]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -237,42 +246,42 @@ class Sanic(
|
||||||
def __init__(
|
def __init__(
|
||||||
self: Sanic[config_type, ctx_type],
|
self: Sanic[config_type, ctx_type],
|
||||||
name: str,
|
name: str,
|
||||||
config: config_type | None = None,
|
config: Optional[config_type] = None,
|
||||||
ctx: ctx_type | None = None,
|
ctx: Optional[ctx_type] = None,
|
||||||
router: Router | None = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: SignalRouter | None = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: ErrorHandler | None = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
env_prefix: str | None = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
request_class: type[Request] | None = None,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
log_config: dict[str, Any] | None = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
dumps: Callable[..., AnyStr] | None = None,
|
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||||
loads: Callable[..., Any] | None = None,
|
loads: Optional[Callable[..., Any]] = None,
|
||||||
inspector: bool = False,
|
inspector: bool = False,
|
||||||
inspector_class: type[Inspector] | None = None,
|
inspector_class: Optional[Type[Inspector]] = None,
|
||||||
certloader_class: type[CertLoader] | None = None,
|
certloader_class: Optional[Type[CertLoader]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
config: config_type | None = None,
|
config: Optional[config_type] = None,
|
||||||
ctx: ctx_type | None = None,
|
ctx: Optional[ctx_type] = None,
|
||||||
router: Router | None = None,
|
router: Optional[Router] = None,
|
||||||
signal_router: SignalRouter | None = None,
|
signal_router: Optional[SignalRouter] = None,
|
||||||
error_handler: ErrorHandler | None = None,
|
error_handler: Optional[ErrorHandler] = None,
|
||||||
env_prefix: str | None = SANIC_PREFIX,
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
request_class: type[Request] | None = None,
|
request_class: Optional[Type[Request]] = None,
|
||||||
strict_slashes: bool = False,
|
strict_slashes: bool = False,
|
||||||
log_config: dict[str, Any] | None = None,
|
log_config: Optional[Dict[str, Any]] = None,
|
||||||
configure_logging: bool = True,
|
configure_logging: bool = True,
|
||||||
dumps: Callable[..., AnyStr] | None = None,
|
dumps: Optional[Callable[..., AnyStr]] = None,
|
||||||
loads: Callable[..., Any] | None = None,
|
loads: Optional[Callable[..., Any]] = None,
|
||||||
inspector: bool = False,
|
inspector: bool = False,
|
||||||
inspector_class: type[Inspector] | None = None,
|
inspector_class: Optional[Type[Inspector]] = None,
|
||||||
certloader_class: type[CertLoader] | None = None,
|
certloader_class: Optional[Type[CertLoader]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
# logging
|
# logging
|
||||||
|
@ -294,39 +303,41 @@ class Sanic(
|
||||||
self.config.INSPECTOR = inspector
|
self.config.INSPECTOR = inspector
|
||||||
|
|
||||||
# Then we can do the rest
|
# Then we can do the rest
|
||||||
self._asgi_app: ASGIApp | None = None
|
self._asgi_app: Optional[ASGIApp] = None
|
||||||
self._asgi_lifespan: Lifespan | None = None
|
self._asgi_lifespan: Optional[Lifespan] = None
|
||||||
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: Inspector | None = None
|
self._inspector: Optional[Inspector] = None
|
||||||
self._manager: WorkerManager | None = None
|
self._manager: Optional[WorkerManager] = None
|
||||||
self._state: ApplicationState = ApplicationState(app=self)
|
self._state: ApplicationState = ApplicationState(app=self)
|
||||||
self._task_registry: dict[str, Task | None] = {}
|
self._task_registry: Dict[str, Union[Task, None]] = {}
|
||||||
self._test_client: Any = None
|
self._test_client: Any = None
|
||||||
self._test_manager: Any = None
|
self._test_manager: Any = None
|
||||||
self.asgi = False
|
self.asgi = False
|
||||||
self.auto_reload = False
|
self.auto_reload = False
|
||||||
self.blueprints: dict[str, Blueprint] = {}
|
self.blueprints: Dict[str, Blueprint] = {}
|
||||||
self.certloader_class: type[CertLoader] = certloader_class or CertLoader
|
self.certloader_class: Type[CertLoader] = (
|
||||||
|
certloader_class or CertLoader
|
||||||
|
)
|
||||||
self.configure_logging: bool = configure_logging
|
self.configure_logging: bool = configure_logging
|
||||||
self.ctx: ctx_type = cast(ctx_type, ctx or SimpleNamespace())
|
self.ctx: ctx_type = cast(ctx_type, ctx or SimpleNamespace())
|
||||||
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
||||||
self.inspector_class: type[Inspector] = inspector_class or Inspector
|
self.inspector_class: Type[Inspector] = inspector_class or Inspector
|
||||||
self.listeners: dict[str, list[ListenerType[Any]]] = defaultdict(list)
|
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
|
||||||
self.named_request_middleware: dict[str, deque[Middleware]] = {}
|
self.named_request_middleware: Dict[str, Deque[Middleware]] = {}
|
||||||
self.named_response_middleware: dict[str, deque[Middleware]] = {}
|
self.named_response_middleware: Dict[str, Deque[Middleware]] = {}
|
||||||
self.request_class: type[Request] = request_class or Request
|
self.request_class: Type[Request] = request_class or Request
|
||||||
self.request_middleware: deque[Middleware] = deque()
|
self.request_middleware: Deque[Middleware] = deque()
|
||||||
self.response_middleware: deque[Middleware] = deque()
|
self.response_middleware: Deque[Middleware] = deque()
|
||||||
self.router: Router = router or Router()
|
self.router: Router = router or Router()
|
||||||
self.shared_ctx: SharedContext = SharedContext()
|
self.shared_ctx: SharedContext = SharedContext()
|
||||||
self.signal_router: SignalRouter = signal_router or SignalRouter()
|
self.signal_router: SignalRouter = signal_router or SignalRouter()
|
||||||
self.sock: socket | None = None
|
self.sock: Optional[socket] = None
|
||||||
self.strict_slashes: bool = strict_slashes
|
self.strict_slashes: bool = strict_slashes
|
||||||
self.websocket_enabled: bool = False
|
self.websocket_enabled: bool = False
|
||||||
self.websocket_tasks: set[Future[Any]] = set()
|
self.websocket_tasks: Set[Future[Any]] = set()
|
||||||
|
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
@ -385,11 +396,15 @@ class Sanic(
|
||||||
try:
|
try:
|
||||||
_event = ListenerEvent[event.upper()]
|
_event = ListenerEvent[event.upper()]
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
valid = ", ".join(x.lower() for x in ListenerEvent.__members__.keys())
|
valid = ", ".join(
|
||||||
|
map(lambda x: x.lower(), ListenerEvent.__members__.keys())
|
||||||
|
)
|
||||||
raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")
|
raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")
|
||||||
|
|
||||||
if "." in _event:
|
if "." in _event:
|
||||||
self.signal(_event.value)(partial(self._listener, listener=listener))
|
self.signal(_event.value)(
|
||||||
|
partial(self._listener, listener=listener)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.listeners[_event.value].append(listener)
|
self.listeners[_event.value].append(listener)
|
||||||
|
|
||||||
|
@ -397,11 +412,11 @@ class Sanic(
|
||||||
|
|
||||||
def register_middleware(
|
def register_middleware(
|
||||||
self,
|
self,
|
||||||
middleware: MiddlewareType | Middleware,
|
middleware: Union[MiddlewareType, Middleware],
|
||||||
attach_to: str = "request",
|
attach_to: str = "request",
|
||||||
*,
|
*,
|
||||||
priority: Default | int = _default,
|
priority: Union[Default, int] = _default,
|
||||||
) -> MiddlewareType | Middleware:
|
) -> Union[MiddlewareType, Middleware]:
|
||||||
"""Register a middleware to be called before a request is handled.
|
"""Register a middleware to be called before a request is handled.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -446,7 +461,7 @@ class Sanic(
|
||||||
route_names: Iterable[str],
|
route_names: Iterable[str],
|
||||||
attach_to: str = "request",
|
attach_to: str = "request",
|
||||||
*,
|
*,
|
||||||
priority: Default | int = _default,
|
priority: Union[Default, int] = _default,
|
||||||
):
|
):
|
||||||
"""Used to register named middleqare (middleware typically on blueprints)
|
"""Used to register named middleqare (middleware typically on blueprints)
|
||||||
|
|
||||||
|
@ -497,7 +512,7 @@ class Sanic(
|
||||||
def _apply_exception_handler(
|
def _apply_exception_handler(
|
||||||
self,
|
self,
|
||||||
handler: FutureException,
|
handler: FutureException,
|
||||||
route_names: list[str] | None = None,
|
route_names: Optional[List[str]] = None,
|
||||||
):
|
):
|
||||||
"""Decorate a function to be registered as a handler for exceptions
|
"""Decorate a function to be registered as a handler for exceptions
|
||||||
|
|
||||||
|
@ -516,7 +531,9 @@ class Sanic(
|
||||||
def _apply_listener(self, listener: FutureListener):
|
def _apply_listener(self, listener: FutureListener):
|
||||||
return self.register_listener(listener.listener, listener.event)
|
return self.register_listener(listener.listener, listener.event)
|
||||||
|
|
||||||
def _apply_route(self, route: FutureRoute, overwrite: bool = False) -> list[Route]:
|
def _apply_route(
|
||||||
|
self, route: FutureRoute, overwrite: bool = False
|
||||||
|
) -> List[Route]:
|
||||||
params = route._asdict()
|
params = route._asdict()
|
||||||
params["overwrite"] = overwrite
|
params["overwrite"] = overwrite
|
||||||
websocket = params.pop("websocket", False)
|
websocket = params.pop("websocket", False)
|
||||||
|
@ -550,7 +567,7 @@ class Sanic(
|
||||||
def _apply_middleware(
|
def _apply_middleware(
|
||||||
self,
|
self,
|
||||||
middleware: FutureMiddleware,
|
middleware: FutureMiddleware,
|
||||||
route_names: list[str] | None = None,
|
route_names: Optional[List[str]] = None,
|
||||||
):
|
):
|
||||||
with self.amend():
|
with self.amend():
|
||||||
if route_names:
|
if route_names:
|
||||||
|
@ -571,8 +588,8 @@ class Sanic(
|
||||||
self,
|
self,
|
||||||
event: str,
|
event: str,
|
||||||
*,
|
*,
|
||||||
condition: dict[str, str] | None = None,
|
condition: Optional[Dict[str, str]] = None,
|
||||||
context: dict[str, Any] | None = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
fail_not_found: bool = True,
|
fail_not_found: bool = True,
|
||||||
inline: Literal[True],
|
inline: Literal[True],
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
|
@ -584,8 +601,8 @@ class Sanic(
|
||||||
self,
|
self,
|
||||||
event: str,
|
event: str,
|
||||||
*,
|
*,
|
||||||
condition: dict[str, str] | None = None,
|
condition: Optional[Dict[str, str]] = None,
|
||||||
context: dict[str, Any] | None = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
fail_not_found: bool = True,
|
fail_not_found: bool = True,
|
||||||
inline: Literal[False] = False,
|
inline: Literal[False] = False,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
|
@ -596,12 +613,12 @@ class Sanic(
|
||||||
self,
|
self,
|
||||||
event: str,
|
event: str,
|
||||||
*,
|
*,
|
||||||
condition: dict[str, str] | None = None,
|
condition: Optional[Dict[str, str]] = None,
|
||||||
context: dict[str, Any] | None = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
fail_not_found: bool = True,
|
fail_not_found: bool = True,
|
||||||
inline: bool = False,
|
inline: bool = False,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
) -> Coroutine[Any, Any, Awaitable[Task | Any]]:
|
) -> Coroutine[Any, Any, Awaitable[Union[Task, Any]]]:
|
||||||
"""Dispatches an event to the signal router.
|
"""Dispatches an event to the signal router.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -645,7 +662,9 @@ class Sanic(
|
||||||
fail_not_found=fail_not_found,
|
fail_not_found=fail_not_found,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def event(self, event: str, timeout: int | float | None = None) -> None:
|
async def event(
|
||||||
|
self, event: str, timeout: Optional[Union[int, float]] = None
|
||||||
|
) -> None:
|
||||||
"""Wait for a specific event to be triggered.
|
"""Wait for a specific event to be triggered.
|
||||||
|
|
||||||
This method waits for a named event to be triggered and can be used
|
This method waits for a named event to be triggered and can be used
|
||||||
|
@ -730,7 +749,9 @@ class Sanic(
|
||||||
async def report(exception: Exception) -> None:
|
async def report(exception: Exception) -> None:
|
||||||
await handler(self, exception)
|
await handler(self, exception)
|
||||||
|
|
||||||
self.add_signal(handler=report, event=Event.SERVER_EXCEPTION_REPORT.value)
|
self.add_signal(
|
||||||
|
handler=report, event=Event.SERVER_EXCEPTION_REPORT.value
|
||||||
|
)
|
||||||
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
@ -759,13 +780,13 @@ class Sanic(
|
||||||
|
|
||||||
def blueprint(
|
def blueprint(
|
||||||
self,
|
self,
|
||||||
blueprint: Blueprint | (Iterable[Blueprint] | BlueprintGroup),
|
blueprint: Union[Blueprint, Iterable[Blueprint], BlueprintGroup],
|
||||||
*,
|
*,
|
||||||
url_prefix: str | None = None,
|
url_prefix: Optional[str] = None,
|
||||||
version: int | float | str | None = None,
|
version: Optional[Union[int, float, str]] = None,
|
||||||
strict_slashes: bool | None = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version_prefix: str | None = None,
|
version_prefix: Optional[str] = None,
|
||||||
name_prefix: str | None = None,
|
name_prefix: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a blueprint on the application.
|
"""Register a blueprint on the application.
|
||||||
|
|
||||||
|
@ -791,7 +812,7 @@ class Sanic(
|
||||||
app.blueprint(bp, url_prefix='/blueprint')
|
app.blueprint(bp, url_prefix='/blueprint')
|
||||||
```
|
```
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
options: dict[str, Any] = {}
|
options: Dict[str, Any] = {}
|
||||||
if url_prefix is not None:
|
if url_prefix is not None:
|
||||||
options["url_prefix"] = url_prefix
|
options["url_prefix"] = url_prefix
|
||||||
if version is not None:
|
if version is not None:
|
||||||
|
@ -804,7 +825,7 @@ class Sanic(
|
||||||
options["name_prefix"] = name_prefix
|
options["name_prefix"] = name_prefix
|
||||||
if isinstance(blueprint, (Iterable, BlueprintGroup)):
|
if isinstance(blueprint, (Iterable, BlueprintGroup)):
|
||||||
for item in blueprint:
|
for item in blueprint:
|
||||||
params: dict[str, Any] = {**options}
|
params: Dict[str, Any] = {**options}
|
||||||
if isinstance(blueprint, BlueprintGroup):
|
if isinstance(blueprint, BlueprintGroup):
|
||||||
merge_from = [
|
merge_from = [
|
||||||
options.get("url_prefix", ""),
|
options.get("url_prefix", ""),
|
||||||
|
@ -819,12 +840,14 @@ class Sanic(
|
||||||
|
|
||||||
for _attr in ["version", "strict_slashes"]:
|
for _attr in ["version", "strict_slashes"]:
|
||||||
if getattr(item, _attr) is None:
|
if getattr(item, _attr) is None:
|
||||||
params[_attr] = getattr(blueprint, _attr) or options.get(
|
params[_attr] = getattr(
|
||||||
_attr
|
blueprint, _attr
|
||||||
)
|
) or options.get(_attr)
|
||||||
if item.version_prefix == "/v":
|
if item.version_prefix == "/v":
|
||||||
if blueprint.version_prefix == "/v":
|
if blueprint.version_prefix == "/v":
|
||||||
params["version_prefix"] = options.get("version_prefix")
|
params["version_prefix"] = options.get(
|
||||||
|
"version_prefix"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
params["version_prefix"] = blueprint.version_prefix
|
params["version_prefix"] = blueprint.version_prefix
|
||||||
name_prefix = getattr(blueprint, "name_prefix", None)
|
name_prefix = getattr(blueprint, "name_prefix", None)
|
||||||
|
@ -834,14 +857,17 @@ class Sanic(
|
||||||
return
|
return
|
||||||
if blueprint.name in self.blueprints:
|
if blueprint.name in self.blueprints:
|
||||||
assert self.blueprints[blueprint.name] is blueprint, (
|
assert self.blueprints[blueprint.name] is blueprint, (
|
||||||
f'A blueprint with the name "{blueprint.name}" is already registered. '
|
'A blueprint with the name "%s" is already registered. '
|
||||||
"Blueprint names must be unique."
|
"Blueprint names must be unique." % (blueprint.name,)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.blueprints[blueprint.name] = blueprint
|
self.blueprints[blueprint.name] = blueprint
|
||||||
self._blueprint_order.append(blueprint)
|
self._blueprint_order.append(blueprint)
|
||||||
|
|
||||||
if self.strict_slashes is not None and blueprint.strict_slashes is None:
|
if (
|
||||||
|
self.strict_slashes is not None
|
||||||
|
and blueprint.strict_slashes is None
|
||||||
|
):
|
||||||
blueprint.strict_slashes = self.strict_slashes
|
blueprint.strict_slashes = self.strict_slashes
|
||||||
blueprint.register(self, options)
|
blueprint.register(self, options)
|
||||||
|
|
||||||
|
@ -897,7 +923,7 @@ class Sanic(
|
||||||
# http://subdomain.example.com/view-name
|
# http://subdomain.example.com/view-name
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
# find the route by the supplied view name
|
# find the route by the supplied view name
|
||||||
kw: dict[str, str] = {}
|
kw: Dict[str, str] = {}
|
||||||
# special static files url_for
|
# special static files url_for
|
||||||
|
|
||||||
if "." not in view_name:
|
if "." not in view_name:
|
||||||
|
@ -911,7 +937,9 @@ class Sanic(
|
||||||
|
|
||||||
route = self.router.find_route_by_view_name(view_name, **kw)
|
route = self.router.find_route_by_view_name(view_name, **kw)
|
||||||
if not route:
|
if not route:
|
||||||
raise URLBuildError(f"Endpoint with name `{view_name}` was not found")
|
raise URLBuildError(
|
||||||
|
f"Endpoint with name `{view_name}` was not found"
|
||||||
|
)
|
||||||
|
|
||||||
uri = route.path
|
uri = route.path
|
||||||
|
|
||||||
|
@ -950,7 +978,9 @@ class Sanic(
|
||||||
scheme = kwargs.pop("_scheme", "")
|
scheme = kwargs.pop("_scheme", "")
|
||||||
if route.extra.hosts and external:
|
if route.extra.hosts and external:
|
||||||
if not host and len(route.extra.hosts) > 1:
|
if not host and len(route.extra.hosts) > 1:
|
||||||
raise ValueError(f"Host is ambiguous: {', '.join(route.extra.hosts)}")
|
raise ValueError(
|
||||||
|
f"Host is ambiguous: {', '.join(route.extra.hosts)}"
|
||||||
|
)
|
||||||
elif host and host not in route.extra.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 "
|
||||||
|
@ -1066,7 +1096,10 @@ class Sanic(
|
||||||
context={"request": request, "exception": exception},
|
context={"request": request, "exception": exception},
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.stream is not None and request.stream.stage is not Stage.HANDLER:
|
if (
|
||||||
|
request.stream is not None
|
||||||
|
and request.stream.stage is not Stage.HANDLER
|
||||||
|
):
|
||||||
error_logger.exception(exception, exc_info=True)
|
error_logger.exception(exception, exc_info=True)
|
||||||
logger.error(
|
logger.error(
|
||||||
"The error response will not be sent to the client for "
|
"The error response will not be sent to the client for "
|
||||||
|
@ -1113,7 +1146,10 @@ class Sanic(
|
||||||
response = self.error_handler.default(request, e)
|
response = self.error_handler.default(request, e)
|
||||||
elif self.debug:
|
elif self.debug:
|
||||||
response = HTTPResponse(
|
response = HTTPResponse(
|
||||||
(f"Error while handling error: {e}\n" f"Stack: {format_exc()}"),
|
(
|
||||||
|
f"Error while handling error: {e}\n"
|
||||||
|
f"Stack: {format_exc()}"
|
||||||
|
),
|
||||||
status=500,
|
status=500,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -1158,7 +1194,9 @@ class Sanic(
|
||||||
)
|
)
|
||||||
await response.eof()
|
await response.eof()
|
||||||
else:
|
else:
|
||||||
raise ServerError(f"Invalid response type {response!r} (need HTTPResponse)")
|
raise ServerError(
|
||||||
|
f"Invalid response type {response!r} (need HTTPResponse)"
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_request(self, request: Request) -> None: # no cov
|
async def handle_request(self, request: Request) -> None: # no cov
|
||||||
"""Handles a request by dispatching it to the appropriate handler.
|
"""Handles a request by dispatching it to the appropriate handler.
|
||||||
|
@ -1183,11 +1221,13 @@ class Sanic(
|
||||||
|
|
||||||
# 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: (
|
response: Optional[
|
||||||
BaseHTTPResponse
|
Union[
|
||||||
| (Coroutine[Any, Any, BaseHTTPResponse | None] | ResponseStream)
|
BaseHTTPResponse,
|
||||||
| None
|
Coroutine[Any, Any, Optional[BaseHTTPResponse]],
|
||||||
) = None
|
ResponseStream,
|
||||||
|
]
|
||||||
|
] = None
|
||||||
run_middleware = True
|
run_middleware = True
|
||||||
try:
|
try:
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
|
@ -1245,9 +1285,11 @@ class Sanic(
|
||||||
|
|
||||||
if handler is None:
|
if handler is None:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
|
(
|
||||||
"'None' was returned while requesting a "
|
"'None' was returned while requesting a "
|
||||||
"handler from the router"
|
"handler from the router"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Run response handler
|
# Run response handler
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
|
@ -1305,14 +1347,17 @@ class Sanic(
|
||||||
else:
|
else:
|
||||||
if not hasattr(handler, "is_websocket"):
|
if not hasattr(handler, "is_websocket"):
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
f"Invalid response type {response!r} " "(need HTTPResponse)"
|
f"Invalid response type {response!r} "
|
||||||
|
"(need HTTPResponse)"
|
||||||
)
|
)
|
||||||
|
|
||||||
except CancelledError: # type: ignore
|
except CancelledError: # type: ignore
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Response Generation Failed
|
# Response Generation Failed
|
||||||
await self.handle_exception(request, e, run_middleware=run_middleware)
|
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
|
||||||
|
@ -1391,7 +1436,9 @@ class Sanic(
|
||||||
# Execution
|
# Execution
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
async def _run_request_middleware(self, request, middleware_collection): # no cov
|
async def _run_request_middleware(
|
||||||
|
self, request, middleware_collection
|
||||||
|
): # no cov
|
||||||
request._request_middleware_started = True
|
request._request_middleware_started = True
|
||||||
|
|
||||||
for middleware in middleware_collection:
|
for middleware in middleware_collection:
|
||||||
|
@ -1468,7 +1515,9 @@ class Sanic(
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _listener(app: Sanic, loop: AbstractEventLoop, listener: ListenerType):
|
async def _listener(
|
||||||
|
app: Sanic, loop: AbstractEventLoop, listener: ListenerType
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
maybe_coro = listener(app) # type: ignore
|
maybe_coro = listener(app) # type: ignore
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -1497,7 +1546,9 @@ class Sanic(
|
||||||
if isawaitable(task):
|
if isawaitable(task):
|
||||||
await task
|
await task
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
error_logger.warning(f"Task {task} was cancelled before it completed.")
|
error_logger.warning(
|
||||||
|
f"Task {task} was cancelled before it completed."
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await app.dispatch(
|
await app.dispatch(
|
||||||
|
@ -1515,7 +1566,7 @@ class Sanic(
|
||||||
app,
|
app,
|
||||||
loop,
|
loop,
|
||||||
*,
|
*,
|
||||||
name: str | None = None,
|
name: Optional[str] = None,
|
||||||
register: bool = True,
|
register: bool = True,
|
||||||
) -> Task:
|
) -> Task:
|
||||||
if not isinstance(task, Future):
|
if not isinstance(task, Future):
|
||||||
|
@ -1577,11 +1628,11 @@ class Sanic(
|
||||||
|
|
||||||
def add_task(
|
def add_task(
|
||||||
self,
|
self,
|
||||||
task: Future[Any] | (Coroutine[Any, Any, Any] | Awaitable[Any]),
|
task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]],
|
||||||
*,
|
*,
|
||||||
name: str | None = None,
|
name: Optional[str] = None,
|
||||||
register: bool = True,
|
register: bool = True,
|
||||||
) -> Task[Any] | None:
|
) -> 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.
|
||||||
|
|
||||||
While this is somewhat similar to `asyncio.create_task`, it can be
|
While this is somewhat similar to `asyncio.create_task`, it can be
|
||||||
|
@ -1606,14 +1657,18 @@ class Sanic(
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
try:
|
try:
|
||||||
loop = self.loop # Will raise SanicError if loop is not started
|
loop = self.loop # Will raise SanicError if loop is not started
|
||||||
return self._loop_add_task(task, self, loop, name=name, register=register)
|
return self._loop_add_task(
|
||||||
|
task, self, loop, name=name, register=register
|
||||||
|
)
|
||||||
except SanicException:
|
except SanicException:
|
||||||
task_name = f"sanic.delayed_task.{hash(task)}"
|
task_name = f"sanic.delayed_task.{hash(task)}"
|
||||||
if not self._delayed_tasks:
|
if not self._delayed_tasks:
|
||||||
self.after_server_start(partial(self.dispatch_delayed_tasks))
|
self.after_server_start(partial(self.dispatch_delayed_tasks))
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
raise RuntimeError("Cannot name task outside of a running application")
|
raise RuntimeError(
|
||||||
|
"Cannot name task outside of a running application"
|
||||||
|
)
|
||||||
|
|
||||||
self.signal(task_name)(partial(self.run_delayed_task, task=task))
|
self.signal(task_name)(partial(self.run_delayed_task, task=task))
|
||||||
self._delayed_tasks.append(task_name)
|
self._delayed_tasks.append(task_name)
|
||||||
|
@ -1624,14 +1679,18 @@ class Sanic(
|
||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_task(self, name: str, *, raise_exception: Literal[False]) -> Task | None:
|
def get_task(
|
||||||
|
self, name: str, *, raise_exception: Literal[False]
|
||||||
|
) -> Optional[Task]:
|
||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_task(self, name: str, *, raise_exception: bool) -> Task | None:
|
def get_task(self, name: str, *, raise_exception: bool) -> Optional[Task]:
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_task(self, name: str, *, raise_exception: bool = True) -> Task | None:
|
def get_task(
|
||||||
|
self, name: str, *, raise_exception: bool = True
|
||||||
|
) -> Optional[Task]:
|
||||||
"""Get a named task.
|
"""Get a named task.
|
||||||
|
|
||||||
This method is used to get a task by its name. Optionally, you can
|
This method is used to get a task by its name. Optionally, you can
|
||||||
|
@ -1649,13 +1708,15 @@ class Sanic(
|
||||||
return self._task_registry[name]
|
return self._task_registry[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
raise SanicException(f'Registered task named "{name}" not found.')
|
raise SanicException(
|
||||||
|
f'Registered task named "{name}" not found.'
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def cancel_task(
|
async def cancel_task(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
msg: str | None = None,
|
msg: Optional[str] = None,
|
||||||
*,
|
*,
|
||||||
raise_exception: bool = True,
|
raise_exception: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1690,7 +1751,7 @@ class Sanic(
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
task = self.get_task(name, raise_exception=raise_exception)
|
task = self.get_task(name, raise_exception=raise_exception)
|
||||||
if task and not task.cancelled():
|
if task and not task.cancelled():
|
||||||
args: tuple[str, ...] = ()
|
args: Tuple[str, ...] = ()
|
||||||
if msg:
|
if msg:
|
||||||
if sys.version_info >= (3, 9):
|
if sys.version_info >= (3, 9):
|
||||||
args = (msg,)
|
args = (msg,)
|
||||||
|
@ -1723,7 +1784,7 @@ class Sanic(
|
||||||
}
|
}
|
||||||
|
|
||||||
def shutdown_tasks(
|
def shutdown_tasks(
|
||||||
self, timeout: float | None = None, increment: float = 0.1
|
self, timeout: Optional[float] = None, increment: float = 0.1
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Cancel all tasks except the server task.
|
"""Cancel all tasks except the server task.
|
||||||
|
|
||||||
|
@ -1761,7 +1822,11 @@ class Sanic(
|
||||||
Iterable[Task[Any]]: The tasks that are currently registered with
|
Iterable[Task[Any]]: The tasks that are currently registered with
|
||||||
the application.
|
the application.
|
||||||
"""
|
"""
|
||||||
return (task for task in iter(self._task_registry.values()) if task is not None)
|
return (
|
||||||
|
task
|
||||||
|
for task in iter(self._task_registry.values())
|
||||||
|
if task is not None
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# ASGI
|
# ASGI
|
||||||
|
@ -1788,7 +1853,7 @@ class Sanic(
|
||||||
# Configuration
|
# Configuration
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def update_config(self, config: Any) -> None:
|
def update_config(self, config: Union[bytes, str, dict, Any]) -> None:
|
||||||
"""Update the application configuration.
|
"""Update the application configuration.
|
||||||
|
|
||||||
This method is used to update the application configuration. It can
|
This method is used to update the application configuration. It can
|
||||||
|
@ -1798,7 +1863,7 @@ class Sanic(
|
||||||
See [Configuration](/en/guide/deployment/configuration) for details.
|
See [Configuration](/en/guide/deployment/configuration) for details.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config (bytes | str | dict | Any): The configuration object,
|
config (Union[bytes, str, dict, Any]): The configuration object,
|
||||||
dictionary, or path to a configuration file.
|
dictionary, or path to a configuration file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1838,7 +1903,7 @@ class Sanic(
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reload_dirs(self) -> set[Path]:
|
def reload_dirs(self) -> Set[Path]:
|
||||||
"""The directories that are monitored for auto-reload.
|
"""The directories that are monitored for auto-reload.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -1883,9 +1948,9 @@ class Sanic(
|
||||||
def extend(
|
def extend(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
extensions: list[type[Extension]] | None = None,
|
extensions: Optional[List[Type[Extension]]] = None,
|
||||||
built_in_extensions: bool = True,
|
built_in_extensions: bool = True,
|
||||||
config: Config | dict[str, Any] | None = None,
|
config: Optional[Union[Config, Dict[str, Any]]] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Extend:
|
) -> Extend:
|
||||||
"""Extend Sanic with additional functionality using Sanic Extensions.
|
"""Extend Sanic with additional functionality using Sanic Extensions.
|
||||||
|
@ -2003,7 +2068,9 @@ class Sanic(
|
||||||
del cls._app_registry[name]
|
del cls._app_registry[name]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_app(cls, name: str | None = None, *, force_create: bool = False) -> Sanic:
|
def get_app(
|
||||||
|
cls, name: Optional[str] = None, *, force_create: bool = False
|
||||||
|
) -> Sanic:
|
||||||
"""Retrieve an instantiated Sanic instance by name.
|
"""Retrieve an instantiated Sanic instance by name.
|
||||||
|
|
||||||
This method is best used when needing to get access to an already
|
This method is best used when needing to get access to an already
|
||||||
|
@ -2210,7 +2277,9 @@ class Sanic(
|
||||||
self.finalize()
|
self.finalize()
|
||||||
|
|
||||||
route_names = [route.extra.ident for route in self.router.routes]
|
route_names = [route.extra.ident for route in self.router.routes]
|
||||||
duplicates = {name for name in route_names if route_names.count(name) > 1}
|
duplicates = {
|
||||||
|
name for name in route_names if route_names.count(name) > 1
|
||||||
|
}
|
||||||
if duplicates:
|
if duplicates:
|
||||||
names = ", ".join(duplicates)
|
names = ", ".join(duplicates)
|
||||||
message = (
|
message = (
|
||||||
|
@ -2247,7 +2316,7 @@ class Sanic(
|
||||||
self,
|
self,
|
||||||
concern: str,
|
concern: str,
|
||||||
action: str,
|
action: str,
|
||||||
loop: AbstractEventLoop | None = None,
|
loop: Optional[AbstractEventLoop] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
event = f"server.{concern}.{action}"
|
event = f"server.{concern}.{action}"
|
||||||
if action not in ("before", "after") or concern not in (
|
if action not in ("before", "after") or concern not in (
|
||||||
|
@ -2255,7 +2324,9 @@ class Sanic(
|
||||||
"shutdown",
|
"shutdown",
|
||||||
):
|
):
|
||||||
raise SanicException(f"Invalid server event: {event}")
|
raise SanicException(f"Invalid server event: {event}")
|
||||||
logger.debug(f"Triggering server events: {event}", extra={"verbosity": 1})
|
logger.debug(
|
||||||
|
f"Triggering server events: {event}", extra={"verbosity": 1}
|
||||||
|
)
|
||||||
reverse = concern == "shutdown"
|
reverse = concern == "shutdown"
|
||||||
if loop is None:
|
if loop is None:
|
||||||
loop = self.loop
|
loop = self.loop
|
||||||
|
@ -2276,7 +2347,7 @@ class Sanic(
|
||||||
|
|
||||||
def refresh(
|
def refresh(
|
||||||
self,
|
self,
|
||||||
passthru: dict[str, Any] | None = None,
|
passthru: Optional[Dict[str, Any]] = None,
|
||||||
) -> Sanic:
|
) -> Sanic:
|
||||||
"""Refresh the application instance. **This is used internally by Sanic**.
|
"""Refresh the application instance. **This is used internally by Sanic**.
|
||||||
|
|
||||||
|
@ -2321,7 +2392,9 @@ class Sanic(
|
||||||
Inspector: An instance of Inspector.
|
Inspector: An instance of Inspector.
|
||||||
"""
|
"""
|
||||||
if environ.get("SANIC_WORKER_PROCESS") or not self._inspector:
|
if environ.get("SANIC_WORKER_PROCESS") or not self._inspector:
|
||||||
raise SanicException("Can only access the inspector from the main process")
|
raise SanicException(
|
||||||
|
"Can only access the inspector from the main process"
|
||||||
|
)
|
||||||
return self._inspector
|
return self._inspector
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -2354,5 +2427,7 @@ class Sanic(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if environ.get("SANIC_WORKER_PROCESS") or not self._manager:
|
if environ.get("SANIC_WORKER_PROCESS") or not self._manager:
|
||||||
raise SanicException("Can only access the manager from the main process")
|
raise SanicException(
|
||||||
|
"Can only access the manager from the main process"
|
||||||
|
)
|
||||||
return self._manager
|
return self._manager
|
||||||
|
|
|
@ -4,6 +4,7 @@ from contextlib import suppress
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
from sanic.helpers import is_atty
|
from sanic.helpers import is_atty
|
||||||
|
|
||||||
|
|
||||||
BASE_LOGO = """
|
BASE_LOGO = """
|
||||||
|
|
||||||
Sanic
|
Sanic
|
||||||
|
@ -61,7 +63,10 @@ def get_logo(full: bool = False, coffee: bool = False) -> str:
|
||||||
else BASE_LOGO
|
else BASE_LOGO
|
||||||
)
|
)
|
||||||
|
|
||||||
if sys.platform == "darwin" and environ.get("TERM_PROGRAM") == "Apple_Terminal":
|
if (
|
||||||
|
sys.platform == "darwin"
|
||||||
|
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
|
||||||
|
):
|
||||||
logo = ansi_pattern.sub("", logo)
|
logo = ansi_pattern.sub("", logo)
|
||||||
|
|
||||||
return logo
|
return logo
|
||||||
|
|
|
@ -79,7 +79,9 @@ class MOTDTTY(MOTD):
|
||||||
def set_variables(self): # no cov
|
def set_variables(self): # no cov
|
||||||
"""Set the variables used for display."""
|
"""Set the variables used for display."""
|
||||||
fallback = (108, 24)
|
fallback = (108, 24)
|
||||||
terminal_width = max(get_terminal_size(fallback=fallback).columns, fallback[0])
|
terminal_width = max(
|
||||||
|
get_terminal_size(fallback=fallback).columns, fallback[0]
|
||||||
|
)
|
||||||
self.max_value_width = terminal_width - fallback[0] + 36
|
self.max_value_width = terminal_width - fallback[0] + 36
|
||||||
|
|
||||||
self.key_width = 4
|
self.key_width = 4
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
if os.name == "nt": # noqa
|
if os.name == "nt": # noqa
|
||||||
import ctypes # noqa
|
import ctypes # noqa
|
||||||
|
|
||||||
|
@ -45,16 +47,21 @@ class Spinner: # noqa
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cursor():
|
def cursor():
|
||||||
while True:
|
while True:
|
||||||
yield from "|/-\\"
|
for cursor in "|/-\\":
|
||||||
|
yield cursor
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hide():
|
def hide():
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
ci = _CursorInfo()
|
ci = _CursorInfo()
|
||||||
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
||||||
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))
|
ctypes.windll.kernel32.GetConsoleCursorInfo(
|
||||||
|
handle, ctypes.byref(ci)
|
||||||
|
)
|
||||||
ci.visible = False
|
ci.visible = False
|
||||||
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
|
ctypes.windll.kernel32.SetConsoleCursorInfo(
|
||||||
|
handle, ctypes.byref(ci)
|
||||||
|
)
|
||||||
elif os.name == "posix":
|
elif os.name == "posix":
|
||||||
sys.stdout.write("\033[?25l")
|
sys.stdout.write("\033[?25l")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
@ -64,9 +71,13 @@ class Spinner: # noqa
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
ci = _CursorInfo()
|
ci = _CursorInfo()
|
||||||
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
||||||
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))
|
ctypes.windll.kernel32.GetConsoleCursorInfo(
|
||||||
|
handle, ctypes.byref(ci)
|
||||||
|
)
|
||||||
ci.visible = True
|
ci.visible = True
|
||||||
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
|
ctypes.windll.kernel32.SetConsoleCursorInfo(
|
||||||
|
handle, ctypes.byref(ci)
|
||||||
|
)
|
||||||
elif os.name == "posix":
|
elif os.name == "posix":
|
||||||
sys.stdout.write("\033[?25h")
|
sys.stdout.write("\033[?25h")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from ssl import SSLContext
|
from ssl import SSLContext
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
||||||
|
|
||||||
from sanic.application.constants import Mode, Server, ServerStage
|
from sanic.application.constants import Mode, Server, ServerStage
|
||||||
from sanic.log import VerbosityFilter, logger
|
from sanic.log import VerbosityFilter, logger
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
@ -19,9 +21,9 @@ if TYPE_CHECKING:
|
||||||
class ApplicationServerInfo:
|
class ApplicationServerInfo:
|
||||||
"""Information about a server instance."""
|
"""Information about a server instance."""
|
||||||
|
|
||||||
settings: dict[str, Any]
|
settings: Dict[str, Any]
|
||||||
stage: ServerStage = field(default=ServerStage.STOPPED)
|
stage: ServerStage = field(default=ServerStage.STOPPED)
|
||||||
server: AsyncioServer | None = field(default=None)
|
server: Optional[AsyncioServer] = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -38,11 +40,11 @@ class ApplicationState:
|
||||||
fast: bool = field(default=False)
|
fast: bool = field(default=False)
|
||||||
host: str = field(default="")
|
host: str = field(default="")
|
||||||
port: int = field(default=0)
|
port: int = field(default=0)
|
||||||
ssl: SSLContext | None = field(default=None)
|
ssl: Optional[SSLContext] = field(default=None)
|
||||||
sock: socket | None = field(default=None)
|
sock: Optional[socket] = field(default=None)
|
||||||
unix: str | None = field(default=None)
|
unix: Optional[str] = field(default=None)
|
||||||
mode: Mode = field(default=Mode.PRODUCTION)
|
mode: Mode = field(default=Mode.PRODUCTION)
|
||||||
reload_dirs: set[Path] = field(default_factory=set)
|
reload_dirs: Set[Path] = field(default_factory=set)
|
||||||
auto_reload: bool = field(default=False)
|
auto_reload: bool = field(default=False)
|
||||||
server: Server = field(default=Server.SANIC)
|
server: Server = field(default=Server.SANIC)
|
||||||
is_running: bool = field(default=False)
|
is_running: bool = field(default=False)
|
||||||
|
@ -51,7 +53,7 @@ class ApplicationState:
|
||||||
verbosity: int = field(default=0)
|
verbosity: int = field(default=0)
|
||||||
workers: int = field(default=0)
|
workers: int = field(default=0)
|
||||||
primary: bool = field(default=True)
|
primary: bool = field(default=True)
|
||||||
server_info: list[ApplicationServerInfo] = field(default_factory=list)
|
server_info: List[ApplicationServerInfo] = field(default_factory=list)
|
||||||
|
|
||||||
# This property relates to the ApplicationState instance and should
|
# This property relates to the ApplicationState instance and should
|
||||||
# not be changed except in the __post_init__ method
|
# not be changed except in the __post_init__ method
|
||||||
|
@ -62,12 +64,14 @@ class ApplicationState:
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Any) -> None:
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
if self._init and name == "_init":
|
if self._init and name == "_init":
|
||||||
raise RuntimeError("Cannot change the value of _init after instantiation")
|
raise RuntimeError(
|
||||||
|
"Cannot change the value of _init after instantiation"
|
||||||
|
)
|
||||||
super().__setattr__(name, value)
|
super().__setattr__(name, value)
|
||||||
if self._init and hasattr(self, f"set_{name}"):
|
if self._init and hasattr(self, f"set_{name}"):
|
||||||
getattr(self, f"set_{name}")(value)
|
getattr(self, f"set_{name}")(value)
|
||||||
|
|
||||||
def set_mode(self, value: str | Mode):
|
def set_mode(self, value: Union[str, Mode]):
|
||||||
if hasattr(self.app, "error_handler"):
|
if hasattr(self.app, "error_handler"):
|
||||||
self.app.error_handler.debug = self.app.debug
|
self.app.error_handler.debug = self.app.debug
|
||||||
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
||||||
|
@ -103,7 +107,9 @@ class ApplicationState:
|
||||||
|
|
||||||
if all(info.stage is ServerStage.SERVING for info in self.server_info):
|
if all(info.stage is ServerStage.SERVING for info in self.server_info):
|
||||||
return ServerStage.SERVING
|
return ServerStage.SERVING
|
||||||
elif any(info.stage is ServerStage.SERVING for info in self.server_info):
|
elif any(
|
||||||
|
info.stage is ServerStage.SERVING for info in self.server_info
|
||||||
|
):
|
||||||
return ServerStage.PARTIAL
|
return ServerStage.PARTIAL
|
||||||
|
|
||||||
return ServerStage.STOPPED
|
return ServerStage.STOPPED
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
from sanic.exceptions import BadRequest, ServerError
|
from sanic.exceptions import BadRequest, ServerError
|
||||||
|
@ -14,6 +15,7 @@ from sanic.response import BaseHTTPResponse
|
||||||
from sanic.server import ConnInfo
|
from sanic.server import ConnInfo
|
||||||
from sanic.server.websockets.connection import WebSocketConnection
|
from sanic.server.websockets.connection import WebSocketConnection
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
@ -107,9 +109,9 @@ class ASGIApp:
|
||||||
request: Request
|
request: Request
|
||||||
transport: MockTransport
|
transport: MockTransport
|
||||||
lifespan: Lifespan
|
lifespan: Lifespan
|
||||||
ws: WebSocketConnection | None
|
ws: Optional[WebSocketConnection]
|
||||||
stage: Stage
|
stage: Stage
|
||||||
response: BaseHTTPResponse | None
|
response: Optional[BaseHTTPResponse]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(
|
async def create(
|
||||||
|
@ -140,7 +142,9 @@ class ASGIApp:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise BadRequest("Header names can only contain US-ASCII characters")
|
raise BadRequest(
|
||||||
|
"Header names can only contain US-ASCII characters"
|
||||||
|
)
|
||||||
|
|
||||||
if scope["type"] == "http":
|
if scope["type"] == "http":
|
||||||
version = scope["http_version"]
|
version = scope["http_version"]
|
||||||
|
@ -149,7 +153,9 @@ class ASGIApp:
|
||||||
version = "1.1"
|
version = "1.1"
|
||||||
method = "GET"
|
method = "GET"
|
||||||
|
|
||||||
instance.ws = instance.transport.create_websocket_connection(send, receive)
|
instance.ws = instance.transport.create_websocket_connection(
|
||||||
|
send, receive
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ServerError("Received unknown ASGI scope")
|
raise ServerError("Received unknown ASGI scope")
|
||||||
|
|
||||||
|
@ -183,7 +189,7 @@ class ASGIApp:
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
async def read(self) -> bytes | None:
|
async def read(self) -> Optional[bytes]:
|
||||||
"""
|
"""
|
||||||
Read and stream the body in chunks from an incoming ASGI message.
|
Read and stream the body in chunks from an incoming ASGI message.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
|
@ -10,6 +11,7 @@ from sanic.mixins.routes import RouteMixin
|
||||||
from sanic.mixins.signals import SignalMixin
|
from sanic.mixins.signals import SignalMixin
|
||||||
from sanic.mixins.static import StaticMixin
|
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_\-]*$")
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +26,9 @@ class BaseSanic(
|
||||||
):
|
):
|
||||||
__slots__ = ("name",)
|
__slots__ = ("name",)
|
||||||
|
|
||||||
def __init__(self, name: Optional[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:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
from .blueprints import BlueprintGroup
|
from .blueprints import BlueprintGroup
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["BlueprintGroup"] # noqa: F405
|
__all__ = ["BlueprintGroup"] # noqa: F405
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import MutableSequence
|
from collections.abc import MutableSequence
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
@ -13,9 +14,15 @@ from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,6 +39,7 @@ from sanic.models.handler_types import (
|
||||||
RouteHandler,
|
RouteHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
||||||
|
@ -114,10 +122,10 @@ class Blueprint(BaseSanic):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
url_prefix: str | None = None,
|
url_prefix: Optional[str] = None,
|
||||||
host: list[str] | str | None = None,
|
host: Optional[Union[List[str], str]] = None,
|
||||||
version: int | str | float | None = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
strict_slashes: bool | None = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
):
|
):
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
|
@ -128,7 +136,9 @@ class Blueprint(BaseSanic):
|
||||||
self.host = host
|
self.host = host
|
||||||
self.strict_slashes = strict_slashes
|
self.strict_slashes = strict_slashes
|
||||||
self.url_prefix = (
|
self.url_prefix = (
|
||||||
url_prefix[:-1] if url_prefix and url_prefix.endswith("/") else url_prefix
|
url_prefix[:-1]
|
||||||
|
if url_prefix and url_prefix.endswith("/")
|
||||||
|
else url_prefix
|
||||||
)
|
)
|
||||||
self.version = version
|
self.version = version
|
||||||
self.version_prefix = version_prefix
|
self.version_prefix = version_prefix
|
||||||
|
@ -151,7 +161,7 @@ class Blueprint(BaseSanic):
|
||||||
return f"Blueprint({args})"
|
return f"Blueprint({args})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def apps(self) -> set[Sanic]:
|
def apps(self) -> Set[Sanic]:
|
||||||
"""Get the set of apps that this blueprint is registered to.
|
"""Get the set of apps that this blueprint is registered to.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -162,7 +172,9 @@ class Blueprint(BaseSanic):
|
||||||
an app.
|
an app.
|
||||||
"""
|
"""
|
||||||
if not self._apps:
|
if not self._apps:
|
||||||
raise SanicException(f"{self} has not yet been registered to an app")
|
raise SanicException(
|
||||||
|
f"{self} has not yet been registered to an app"
|
||||||
|
)
|
||||||
return self._apps
|
return self._apps
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -184,23 +196,23 @@ class Blueprint(BaseSanic):
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the blueprint to its initial state."""
|
"""Reset the blueprint to its initial state."""
|
||||||
self._apps: set[Sanic] = set()
|
self._apps: Set[Sanic] = set()
|
||||||
self._allow_route_overwrite = False
|
self._allow_route_overwrite = False
|
||||||
self.exceptions: list[RouteHandler] = []
|
self.exceptions: List[RouteHandler] = []
|
||||||
self.listeners: dict[str, list[ListenerType[Any]]] = {}
|
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
|
||||||
self.middlewares: list[MiddlewareType] = []
|
self.middlewares: List[MiddlewareType] = []
|
||||||
self.routes: list[Route] = []
|
self.routes: List[Route] = []
|
||||||
self.statics: list[RouteHandler] = []
|
self.statics: List[RouteHandler] = []
|
||||||
self.websocket_routes: list[Route] = []
|
self.websocket_routes: List[Route] = []
|
||||||
|
|
||||||
def copy(
|
def copy(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
url_prefix: str | Default | None = _default,
|
url_prefix: Optional[Union[str, Default]] = _default,
|
||||||
version: int | str | float | Default | None = _default,
|
version: Optional[Union[int, str, float, Default]] = _default,
|
||||||
version_prefix: str | Default = _default,
|
version_prefix: Union[str, Default] = _default,
|
||||||
allow_route_overwrite: bool | Default = _default,
|
allow_route_overwrite: Union[bool, Default] = _default,
|
||||||
strict_slashes: bool | Default | None = _default,
|
strict_slashes: Optional[Union[bool, Default]] = _default,
|
||||||
with_registration: bool = True,
|
with_registration: bool = True,
|
||||||
with_ctx: bool = False,
|
with_ctx: bool = False,
|
||||||
):
|
):
|
||||||
|
@ -265,12 +277,12 @@ class Blueprint(BaseSanic):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def group(
|
def group(
|
||||||
*blueprints: Blueprint | BlueprintGroup,
|
*blueprints: Union[Blueprint, BlueprintGroup],
|
||||||
url_prefix: str | None = None,
|
url_prefix: Optional[str] = None,
|
||||||
version: int | str | float | None = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
strict_slashes: bool | None = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
name_prefix: str | None = "",
|
name_prefix: Optional[str] = "",
|
||||||
) -> BlueprintGroup:
|
) -> BlueprintGroup:
|
||||||
"""Group multiple blueprints (or other blueprint groups) together.
|
"""Group multiple blueprints (or other blueprint groups) together.
|
||||||
|
|
||||||
|
@ -341,7 +353,9 @@ class Blueprint(BaseSanic):
|
||||||
opt_strict_slashes = options.get("strict_slashes", None)
|
opt_strict_slashes = options.get("strict_slashes", None)
|
||||||
opt_version_prefix = options.get("version_prefix", self.version_prefix)
|
opt_version_prefix = options.get("version_prefix", self.version_prefix)
|
||||||
opt_name_prefix = options.get("name_prefix", None)
|
opt_name_prefix = options.get("name_prefix", None)
|
||||||
error_format = options.get("error_format", app.config.FALLBACK_ERROR_FORMAT)
|
error_format = options.get(
|
||||||
|
"error_format", app.config.FALLBACK_ERROR_FORMAT
|
||||||
|
)
|
||||||
|
|
||||||
routes = []
|
routes = []
|
||||||
middleware = []
|
middleware = []
|
||||||
|
@ -367,7 +381,9 @@ class Blueprint(BaseSanic):
|
||||||
version_prefix = prefix
|
version_prefix = prefix
|
||||||
break
|
break
|
||||||
|
|
||||||
version = self._extract_value(future.version, opt_version, self.version)
|
version = self._extract_value(
|
||||||
|
future.version, opt_version, self.version
|
||||||
|
)
|
||||||
strict_slashes = self._extract_value(
|
strict_slashes = self._extract_value(
|
||||||
future.strict_slashes, opt_strict_slashes, self.strict_slashes
|
future.strict_slashes, opt_strict_slashes, self.strict_slashes
|
||||||
)
|
)
|
||||||
|
@ -403,16 +419,22 @@ class Blueprint(BaseSanic):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
registered.add(apply_route)
|
registered.add(apply_route)
|
||||||
route = app._apply_route(apply_route, overwrite=self._allow_route_overwrite)
|
route = app._apply_route(
|
||||||
|
apply_route, overwrite=self._allow_route_overwrite
|
||||||
|
)
|
||||||
|
|
||||||
# If it is a copied BP, then make sure all of the names of routes
|
# If it is a copied BP, then make sure all of the names of routes
|
||||||
# matchup with the new BP name
|
# matchup with the new BP name
|
||||||
if self.copied_from:
|
if self.copied_from:
|
||||||
for r in route:
|
for r in route:
|
||||||
r.name = r.name.replace(self.copied_from, self.name)
|
r.name = r.name.replace(self.copied_from, self.name)
|
||||||
r.extra.ident = r.extra.ident.replace(self.copied_from, self.name)
|
r.extra.ident = r.extra.ident.replace(
|
||||||
|
self.copied_from, self.name
|
||||||
|
)
|
||||||
|
|
||||||
operation = routes.extend if isinstance(route, list) else routes.append
|
operation = (
|
||||||
|
routes.extend if isinstance(route, list) else routes.append
|
||||||
|
)
|
||||||
operation(route)
|
operation(route)
|
||||||
|
|
||||||
# Static Files
|
# Static Files
|
||||||
|
@ -457,7 +479,7 @@ class Blueprint(BaseSanic):
|
||||||
continue
|
continue
|
||||||
future.condition.update({"__blueprint__": self.name})
|
future.condition.update({"__blueprint__": self.name})
|
||||||
# Force exclusive to be False
|
# Force exclusive to be False
|
||||||
app._apply_signal((*future[:-1], False))
|
app._apply_signal(tuple((*future[:-1], False)))
|
||||||
|
|
||||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||||
self.websocket_routes += [
|
self.websocket_routes += [
|
||||||
|
@ -490,9 +512,11 @@ class Blueprint(BaseSanic):
|
||||||
condition = kwargs.pop("condition", {})
|
condition = kwargs.pop("condition", {})
|
||||||
condition.update({"__blueprint__": self.name})
|
condition.update({"__blueprint__": self.name})
|
||||||
kwargs["condition"] = condition
|
kwargs["condition"] = condition
|
||||||
await asyncio.gather(*[app.dispatch(*args, **kwargs) for app in self.apps])
|
await asyncio.gather(
|
||||||
|
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
||||||
|
)
|
||||||
|
|
||||||
def event(self, event: str, timeout: int | float | None = None):
|
def event(self, event: str, timeout: Optional[Union[int, float]] = None):
|
||||||
"""Wait for a signal event to be dispatched.
|
"""Wait for a signal event to be dispatched.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -526,7 +550,7 @@ class Blueprint(BaseSanic):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _setup_uri(base: str, prefix: str | None):
|
def _setup_uri(base: str, prefix: Optional[str]):
|
||||||
uri = base
|
uri = base
|
||||||
if prefix:
|
if prefix:
|
||||||
uri = prefix
|
uri = prefix
|
||||||
|
@ -539,7 +563,7 @@ class Blueprint(BaseSanic):
|
||||||
|
|
||||||
@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, ...]]
|
||||||
):
|
):
|
||||||
"""Register futures to the apps.
|
"""Register futures to the apps.
|
||||||
|
|
||||||
|
@ -551,7 +575,7 @@ class Blueprint(BaseSanic):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for app in apps:
|
for app in apps:
|
||||||
app._future_registry.update({(bp, item) for item in futures})
|
app._future_registry.update(set((bp, item) for item in futures))
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 9):
|
if sys.version_info < (3, 9):
|
||||||
|
@ -643,13 +667,13 @@ class BlueprintGroup(bpg_base):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
url_prefix: str | None = None,
|
url_prefix: Optional[str] = None,
|
||||||
version: int | str | float | None = None,
|
version: Optional[Union[int, str, float]] = None,
|
||||||
strict_slashes: bool | None = None,
|
strict_slashes: Optional[bool] = None,
|
||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
name_prefix: str | None = "",
|
name_prefix: Optional[str] = "",
|
||||||
):
|
):
|
||||||
self._blueprints: list[Blueprint] = []
|
self._blueprints: List[Blueprint] = []
|
||||||
self._url_prefix = url_prefix
|
self._url_prefix = url_prefix
|
||||||
self._version = version
|
self._version = version
|
||||||
self._version_prefix = version_prefix
|
self._version_prefix = version_prefix
|
||||||
|
@ -657,7 +681,7 @@ class BlueprintGroup(bpg_base):
|
||||||
self._name_prefix = name_prefix
|
self._name_prefix = name_prefix
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url_prefix(self) -> int | str | float | None:
|
def url_prefix(self) -> Optional[Union[int, str, float]]:
|
||||||
"""The URL prefix for the Blueprint Group.
|
"""The URL prefix for the Blueprint Group.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -667,7 +691,7 @@ class BlueprintGroup(bpg_base):
|
||||||
return self._url_prefix
|
return self._url_prefix
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def blueprints(self) -> list[Blueprint]:
|
def blueprints(self) -> List[Blueprint]:
|
||||||
"""A list of all the available blueprints under this group.
|
"""A list of all the available blueprints under this group.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -677,7 +701,7 @@ class BlueprintGroup(bpg_base):
|
||||||
return self._blueprints
|
return self._blueprints
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> str | int | float | None:
|
def version(self) -> Optional[Union[str, int, float]]:
|
||||||
"""API Version for the Blueprint Group, if any.
|
"""API Version for the Blueprint Group, if any.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -686,7 +710,7 @@ class BlueprintGroup(bpg_base):
|
||||||
return self._version
|
return self._version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strict_slashes(self) -> bool | None:
|
def strict_slashes(self) -> Optional[bool]:
|
||||||
"""Whether to enforce strict slashes for the Blueprint Group.
|
"""Whether to enforce strict slashes for the Blueprint Group.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -704,7 +728,7 @@ class BlueprintGroup(bpg_base):
|
||||||
return self._version_prefix
|
return self._version_prefix
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name_prefix(self) -> str | None:
|
def name_prefix(self) -> Optional[str]:
|
||||||
"""Name prefix for the Blueprint Group.
|
"""Name prefix for the Blueprint Group.
|
||||||
|
|
||||||
This is mainly needed when blueprints are copied in order to
|
This is mainly needed when blueprints are copied in order to
|
||||||
|
@ -731,7 +755,9 @@ class BlueprintGroup(bpg_base):
|
||||||
def __getitem__(self, item: slice) -> MutableSequence[Blueprint]:
|
def __getitem__(self, item: slice) -> MutableSequence[Blueprint]:
|
||||||
...
|
...
|
||||||
|
|
||||||
def __getitem__(self, item: int | slice) -> Blueprint | MutableSequence[Blueprint]:
|
def __getitem__(
|
||||||
|
self, item: Union[int, slice]
|
||||||
|
) -> Union[Blueprint, MutableSequence[Blueprint]]:
|
||||||
"""Get the Blueprint object at the specified index.
|
"""Get the Blueprint object at the specified index.
|
||||||
|
|
||||||
This method returns a blueprint inside the group specified by
|
This method returns a blueprint inside the group specified by
|
||||||
|
@ -759,8 +785,8 @@ class BlueprintGroup(bpg_base):
|
||||||
|
|
||||||
def __setitem__(
|
def __setitem__(
|
||||||
self,
|
self,
|
||||||
index: int | slice,
|
index: Union[int, slice],
|
||||||
item: Blueprint | Iterable[Blueprint],
|
item: Union[Blueprint, Iterable[Blueprint]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the Blueprint object at the specified index.
|
"""Set the Blueprint object at the specified index.
|
||||||
|
|
||||||
|
@ -798,7 +824,7 @@ class BlueprintGroup(bpg_base):
|
||||||
def __delitem__(self, index: slice) -> None:
|
def __delitem__(self, index: slice) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
def __delitem__(self, index: int | slice) -> None:
|
def __delitem__(self, index: Union[int, slice]) -> None:
|
||||||
"""Delete the Blueprint object at the specified index.
|
"""Delete the Blueprint object at the specified index.
|
||||||
|
|
||||||
Abstract method implemented to turn the `BlueprintGroup` class
|
Abstract method implemented to turn the `BlueprintGroup` class
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
|
@ -56,7 +57,9 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||||
)
|
)
|
||||||
self.parser._positionals.title = "Required\n========\n Positional"
|
self.parser._positionals.title = "Required\n========\n Positional"
|
||||||
self.parser._optionals.title = "Optional\n========\n General"
|
self.parser._optionals.title = "Optional\n========\n General"
|
||||||
self.main_process = os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
|
self.main_process = (
|
||||||
|
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
|
||||||
|
)
|
||||||
self.args: Namespace = Namespace()
|
self.args: Namespace = Namespace()
|
||||||
self.groups: List[Group] = []
|
self.groups: List[Group] = []
|
||||||
self.inspecting = False
|
self.inspecting = False
|
||||||
|
@ -124,7 +127,11 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||||
key = key.lstrip("-")
|
key = key.lstrip("-")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
value = False if arg.startswith("--no-") else True
|
value = False if arg.startswith("--no-") else True
|
||||||
key = arg.replace("--no-", "").lstrip("-").replace("-", "_")
|
key = (
|
||||||
|
arg.replace("--no-", "")
|
||||||
|
.lstrip("-")
|
||||||
|
.replace("-", "_")
|
||||||
|
)
|
||||||
setattr(self.args, key, value)
|
setattr(self.args, key, value)
|
||||||
|
|
||||||
kwargs = {**self.args.__dict__}
|
kwargs = {**self.args.__dict__}
|
||||||
|
@ -174,7 +181,8 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||||
" Example Module: project.sanic_server.app"
|
" Example Module: project.sanic_server.app"
|
||||||
)
|
)
|
||||||
error_logger.error(
|
error_logger.error(
|
||||||
"\nThe error below might have caused the above one:\n" f"{e.msg}"
|
"\nThe error below might have caused the above one:\n"
|
||||||
|
f"{e.msg}"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
|
@ -188,7 +196,7 @@ Or, a path to a directory to run as a simple HTTP server:
|
||||||
if self.args.tlshost:
|
if self.args.tlshost:
|
||||||
ssl.append(None)
|
ssl.append(None)
|
||||||
if self.args.cert is not None or self.args.key is not None:
|
if self.args.cert is not None or self.args.key is not None:
|
||||||
ssl.append({"cert": self.args.cert, "key": self.args.key})
|
ssl.append(dict(cert=self.args.cert, key=self.args.key))
|
||||||
if self.args.tls:
|
if self.args.tls:
|
||||||
ssl += self.args.tls
|
ssl += self.args.tls
|
||||||
if not ssl:
|
if not ssl:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from argparse import ArgumentParser, _ArgumentGroup
|
from argparse import ArgumentParser, _ArgumentGroup
|
||||||
|
from typing import List, Optional, Type, Union
|
||||||
|
|
||||||
from sanic_routing import __version__ as __routing_version__
|
from sanic_routing import __version__ as __routing_version__
|
||||||
|
|
||||||
|
@ -9,14 +10,14 @@ from sanic.http.constants import HTTP
|
||||||
|
|
||||||
|
|
||||||
class Group:
|
class Group:
|
||||||
name: str | None
|
name: Optional[str]
|
||||||
container: ArgumentParser | _ArgumentGroup
|
container: Union[ArgumentParser, _ArgumentGroup]
|
||||||
_registry: list[type[Group]] = []
|
_registry: List[Type[Group]] = []
|
||||||
|
|
||||||
def __init_subclass__(cls) -> None:
|
def __init_subclass__(cls) -> None:
|
||||||
Group._registry.append(cls)
|
Group._registry.append(cls)
|
||||||
|
|
||||||
def __init__(self, parser: ArgumentParser, title: str | None):
|
def __init__(self, parser: ArgumentParser, title: Optional[str]):
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
|
||||||
if title:
|
if title:
|
||||||
|
@ -244,7 +245,10 @@ class DevelopmentGroup(Group):
|
||||||
"--auto-reload",
|
"--auto-reload",
|
||||||
dest="auto_reload",
|
dest="auto_reload",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help=("Watch source directory for file changes and reload on " "changes"),
|
help=(
|
||||||
|
"Watch source directory for file changes and reload on "
|
||||||
|
"changes"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.container.add_argument(
|
self.container.add_argument(
|
||||||
"-R",
|
"-R",
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from http.client import RemoteDisconnected
|
from http.client import RemoteDisconnected
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
from typing import Any
|
from typing import Any, Dict, Optional
|
||||||
from urllib.error import URLError
|
from urllib.error import URLError
|
||||||
from urllib.request import Request as URequest
|
from urllib.request import Request as URequest
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
@ -12,6 +13,7 @@ from sanic.application.logo import get_logo
|
||||||
from sanic.application.motd import MOTDTTY
|
from sanic.application.motd import MOTDTTY
|
||||||
from sanic.log import Colors
|
from sanic.log import Colors
|
||||||
|
|
||||||
|
|
||||||
try: # no cov
|
try: # no cov
|
||||||
from ujson import dumps, loads
|
from ujson import dumps, loads
|
||||||
except ModuleNotFoundError: # no cov
|
except ModuleNotFoundError: # no cov
|
||||||
|
@ -25,7 +27,7 @@ class InspectorClient:
|
||||||
port: int,
|
port: int,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
raw: bool,
|
raw: bool,
|
||||||
api_key: str | None,
|
api_key: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.scheme = "https" if secure else "http"
|
self.scheme = "https" if secure else "http"
|
||||||
self.host = host
|
self.host = host
|
||||||
|
@ -45,7 +47,11 @@ class InspectorClient:
|
||||||
return
|
return
|
||||||
result = self.request(action, **kwargs).get("result")
|
result = self.request(action, **kwargs).get("result")
|
||||||
if result:
|
if result:
|
||||||
out = dumps(result) if isinstance(result, (list, dict)) else str(result)
|
out = (
|
||||||
|
dumps(result)
|
||||||
|
if isinstance(result, (list, dict))
|
||||||
|
else str(result)
|
||||||
|
)
|
||||||
sys.stdout.write(out + "\n")
|
sys.stdout.write(out + "\n")
|
||||||
|
|
||||||
def info(self) -> None:
|
def info(self) -> None:
|
||||||
|
@ -83,7 +89,7 @@ class InspectorClient:
|
||||||
|
|
||||||
def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any:
|
def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any:
|
||||||
url = f"{self.base_url}/{action}"
|
url = f"{self.base_url}/{action}"
|
||||||
params: dict[str, Any] = {"method": method, "headers": {}}
|
params: Dict[str, Any] = {"method": method, "headers": {}}
|
||||||
if kwargs:
|
if kwargs:
|
||||||
params["data"] = dumps(kwargs).encode()
|
params["data"] = dumps(kwargs).encode()
|
||||||
params["headers"]["content-type"] = "application/json"
|
params["headers"]["content-type"] = "application/json"
|
||||||
|
|
|
@ -3,16 +3,25 @@ import os
|
||||||
import platform
|
import platform
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Awaitable, Literal, Union
|
from typing import Awaitable, Union
|
||||||
|
|
||||||
from multidict import CIMultiDict # type: ignore
|
from multidict import CIMultiDict # type: ignore
|
||||||
|
|
||||||
from sanic.helpers import Default
|
from sanic.helpers import Default
|
||||||
from sanic.log import error_logger
|
from sanic.log import error_logger
|
||||||
|
|
||||||
StartMethod = Union[Default, Literal["fork"], Literal["forkserver"], Literal["spawn"]]
|
|
||||||
|
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"
|
||||||
PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy"
|
PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy"
|
||||||
|
@ -133,10 +142,7 @@ if use_trio: # pragma: no cover
|
||||||
return trio.Path(path).stat()
|
return trio.Path(path).stat()
|
||||||
|
|
||||||
open_async = trio.open_file
|
open_async = trio.open_file
|
||||||
CancelledErrors: tuple[type[BaseException], ...] = (
|
CancelledErrors = tuple([asyncio.CancelledError, trio.Cancelled])
|
||||||
asyncio.CancelledError,
|
|
||||||
trio.Cancelled,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if PYPY_IMPLEMENTATION:
|
if PYPY_IMPLEMENTATION:
|
||||||
pypy_os_module_patch()
|
pypy_os_module_patch()
|
||||||
|
@ -150,7 +156,7 @@ else:
|
||||||
async def open_async(file, mode="r", **kwargs):
|
async def open_async(file, mode="r", **kwargs):
|
||||||
return aio_open(file, mode, **kwargs)
|
return aio_open(file, mode, **kwargs)
|
||||||
|
|
||||||
CancelledErrors = (asyncio.CancelledError,)
|
CancelledErrors = tuple([asyncio.CancelledError])
|
||||||
|
|
||||||
|
|
||||||
def ctrlc_workaround_for_windows(app):
|
def ctrlc_workaround_for_windows(app):
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from abc import ABCMeta
|
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, Literal, Sequence, Union
|
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
||||||
from warnings import filterwarnings
|
from warnings import filterwarnings
|
||||||
|
|
||||||
from sanic.constants import LocalCertCreator
|
from sanic.constants import LocalCertCreator
|
||||||
|
@ -14,6 +16,10 @@ from sanic.http import Http
|
||||||
from sanic.log import 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[
|
FilterWarningType = Union[
|
||||||
Literal["default"],
|
Literal["default"],
|
||||||
Literal["error"],
|
Literal["error"],
|
||||||
|
@ -22,6 +28,8 @@ FilterWarningType = Union[
|
||||||
Literal["module"],
|
Literal["module"],
|
||||||
Literal["once"],
|
Literal["once"],
|
||||||
]
|
]
|
||||||
|
else:
|
||||||
|
FilterWarningType = str
|
||||||
|
|
||||||
SANIC_PREFIX = "SANIC_"
|
SANIC_PREFIX = "SANIC_"
|
||||||
|
|
||||||
|
@ -92,25 +100,25 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||||
EVENT_AUTOREGISTER: bool
|
EVENT_AUTOREGISTER: bool
|
||||||
DEPRECATION_FILTER: FilterWarningType
|
DEPRECATION_FILTER: FilterWarningType
|
||||||
FORWARDED_FOR_HEADER: str
|
FORWARDED_FOR_HEADER: str
|
||||||
FORWARDED_SECRET: str | None
|
FORWARDED_SECRET: Optional[str]
|
||||||
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
GRACEFUL_SHUTDOWN_TIMEOUT: float
|
||||||
INSPECTOR: bool
|
INSPECTOR: bool
|
||||||
INSPECTOR_HOST: str
|
INSPECTOR_HOST: str
|
||||||
INSPECTOR_PORT: int
|
INSPECTOR_PORT: int
|
||||||
INSPECTOR_TLS_KEY: Path | str | Default
|
INSPECTOR_TLS_KEY: Union[Path, str, Default]
|
||||||
INSPECTOR_TLS_CERT: Path | str | Default
|
INSPECTOR_TLS_CERT: Union[Path, str, Default]
|
||||||
INSPECTOR_API_KEY: str
|
INSPECTOR_API_KEY: str
|
||||||
KEEP_ALIVE_TIMEOUT: int
|
KEEP_ALIVE_TIMEOUT: int
|
||||||
KEEP_ALIVE: bool
|
KEEP_ALIVE: bool
|
||||||
LOCAL_CERT_CREATOR: str | LocalCertCreator
|
LOCAL_CERT_CREATOR: Union[str, LocalCertCreator]
|
||||||
LOCAL_TLS_KEY: Path | str | Default
|
LOCAL_TLS_KEY: Union[Path, str, Default]
|
||||||
LOCAL_TLS_CERT: Path | str | Default
|
LOCAL_TLS_CERT: Union[Path, str, Default]
|
||||||
LOCALHOST: str
|
LOCALHOST: str
|
||||||
MOTD: bool
|
MOTD: bool
|
||||||
MOTD_DISPLAY: dict[str, str]
|
MOTD_DISPLAY: Dict[str, str]
|
||||||
NOISY_EXCEPTIONS: bool
|
NOISY_EXCEPTIONS: bool
|
||||||
PROXIES_COUNT: int | None
|
PROXIES_COUNT: Optional[int]
|
||||||
REAL_IP_HEADER: str | None
|
REAL_IP_HEADER: Optional[str]
|
||||||
REQUEST_BUFFER_SIZE: int
|
REQUEST_BUFFER_SIZE: int
|
||||||
REQUEST_MAX_HEADER_SIZE: int
|
REQUEST_MAX_HEADER_SIZE: int
|
||||||
REQUEST_ID_HEADER: str
|
REQUEST_ID_HEADER: str
|
||||||
|
@ -119,19 +127,21 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||||
RESPONSE_TIMEOUT: int
|
RESPONSE_TIMEOUT: int
|
||||||
SERVER_NAME: str
|
SERVER_NAME: str
|
||||||
TLS_CERT_PASSWORD: str
|
TLS_CERT_PASSWORD: str
|
||||||
TOUCHUP: Default | bool
|
TOUCHUP: Union[Default, bool]
|
||||||
USE_UVLOOP: Default | bool
|
USE_UVLOOP: Union[Default, bool]
|
||||||
WEBSOCKET_MAX_SIZE: int
|
WEBSOCKET_MAX_SIZE: int
|
||||||
WEBSOCKET_PING_INTERVAL: int
|
WEBSOCKET_PING_INTERVAL: int
|
||||||
WEBSOCKET_PING_TIMEOUT: int
|
WEBSOCKET_PING_TIMEOUT: int
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
defaults: dict[str, str | bool | int | float | None] | None = None,
|
defaults: Optional[
|
||||||
env_prefix: str | None = SANIC_PREFIX,
|
Dict[str, Union[str, bool, int, float, None]]
|
||||||
keep_alive: bool | None = None,
|
] = None,
|
||||||
|
env_prefix: Optional[str] = SANIC_PREFIX,
|
||||||
|
keep_alive: Optional[bool] = None,
|
||||||
*,
|
*,
|
||||||
converters: Sequence[Callable[[str], Any]] | None = None,
|
converters: Optional[Sequence[Callable[[str], Any]]] = None,
|
||||||
):
|
):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
@ -199,7 +209,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
for k in {**kwargs}.keys()
|
for k in {**kwargs}.keys()
|
||||||
if k in self.__class__.__setters__
|
if k in self.__class__.__setters__
|
||||||
|
@ -227,7 +237,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||||
if attr == "LOCAL_CERT_CREATOR" and not isinstance(
|
if attr == "LOCAL_CERT_CREATOR" and not isinstance(
|
||||||
self.LOCAL_CERT_CREATOR, LocalCertCreator
|
self.LOCAL_CERT_CREATOR, LocalCertCreator
|
||||||
):
|
):
|
||||||
self.LOCAL_CERT_CREATOR = LocalCertCreator[self.LOCAL_CERT_CREATOR.upper()]
|
self.LOCAL_CERT_CREATOR = LocalCertCreator[
|
||||||
|
self.LOCAL_CERT_CREATOR.upper()
|
||||||
|
]
|
||||||
elif attr == "DEPRECATION_FILTER":
|
elif attr == "DEPRECATION_FILTER":
|
||||||
self._configure_warnings()
|
self._configure_warnings()
|
||||||
|
|
||||||
|
@ -264,7 +276,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||||
module=r"sanic.*",
|
module=r"sanic.*",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_error_format(self, format: str | None = 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)
|
||||||
|
|
||||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||||
|
@ -320,7 +332,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_config(self, config: bytes | str | dict | Any):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""Update app.config.
|
"""Update app.config.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
from .response import Cookie, CookieJar
|
from .response import Cookie, CookieJar
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("Cookie", "CookieJar")
|
__all__ = ("Cookie", "CookieJar")
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from sanic.cookies.response import Cookie
|
from sanic.cookies.response import Cookie
|
||||||
from sanic.log import deprecation
|
from sanic.log import deprecation
|
||||||
from sanic.request.parameters import RequestParameters
|
from sanic.request.parameters import RequestParameters
|
||||||
|
|
||||||
|
|
||||||
COOKIE_NAME_RESERVED_CHARS = re.compile(
|
COOKIE_NAME_RESERVED_CHARS = re.compile(
|
||||||
'[\x00-\x1F\x7F-\xFF()<>@,;:\\\\"/[\\]?={} \x09]'
|
'[\x00-\x1F\x7F-\xFF()<>@,;:\\\\"/[\\]?={} \x09]'
|
||||||
)
|
)
|
||||||
|
@ -147,7 +149,9 @@ class CookieRequestParameters(RequestParameters):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return super().get(name, default)
|
return super().get(name, default)
|
||||||
|
|
||||||
def getlist(self, name: str, default: Optional[Any] = None) -> Optional[Any]:
|
def getlist(
|
||||||
|
self, name: str, default: Optional[Any] = None
|
||||||
|
) -> Optional[Any]:
|
||||||
try:
|
try:
|
||||||
return self._get_prefixed_cookie(name)
|
return self._get_prefixed_cookie(name)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -2,15 +2,21 @@ from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
import sys
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Any, Union
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.log import deprecation
|
from sanic.log import deprecation
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
|
|
||||||
|
if sys.version_info < (3, 8): # no cov
|
||||||
|
SameSite = str
|
||||||
|
else: # no cov
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
SameSite = Union[
|
SameSite = Union[
|
||||||
|
@ -174,7 +180,7 @@ class CookieJar(dict):
|
||||||
return CookieJar.HEADER_KEY
|
return CookieJar.HEADER_KEY
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookie_headers(self) -> dict[str, str]: # no cov
|
def cookie_headers(self) -> Dict[str, str]: # no cov
|
||||||
"""Deprecated in v24.3"""
|
"""Deprecated in v24.3"""
|
||||||
deprecation(
|
deprecation(
|
||||||
"The CookieJar.coookie_headers property has been deprecated "
|
"The CookieJar.coookie_headers property has been deprecated "
|
||||||
|
@ -185,7 +191,7 @@ class CookieJar(dict):
|
||||||
return {key: self.header_key for key in self}
|
return {key: self.header_key for key in self}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self) -> list[Cookie]:
|
def cookies(self) -> List[Cookie]:
|
||||||
"""A list of cookies in the CookieJar.
|
"""A list of cookies in the CookieJar.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -197,10 +203,10 @@ class CookieJar(dict):
|
||||||
self,
|
self,
|
||||||
key: str,
|
key: str,
|
||||||
path: str = "/",
|
path: str = "/",
|
||||||
domain: str | None = None,
|
domain: Optional[str] = None,
|
||||||
host_prefix: bool = False,
|
host_prefix: bool = False,
|
||||||
secure_prefix: bool = False,
|
secure_prefix: bool = False,
|
||||||
) -> Cookie | None:
|
) -> Optional[Cookie]:
|
||||||
"""Fetch a cookie from the CookieJar.
|
"""Fetch a cookie from the CookieJar.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -230,7 +236,7 @@ class CookieJar(dict):
|
||||||
self,
|
self,
|
||||||
key: str,
|
key: str,
|
||||||
path: str = "/",
|
path: str = "/",
|
||||||
domain: str | None = None,
|
domain: Optional[str] = None,
|
||||||
host_prefix: bool = False,
|
host_prefix: bool = False,
|
||||||
secure_prefix: bool = False,
|
secure_prefix: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
@ -265,14 +271,14 @@ class CookieJar(dict):
|
||||||
value: str,
|
value: str,
|
||||||
*,
|
*,
|
||||||
path: str = "/",
|
path: str = "/",
|
||||||
domain: str | None = None,
|
domain: Optional[str] = None,
|
||||||
secure: bool = True,
|
secure: bool = True,
|
||||||
max_age: int | None = None,
|
max_age: Optional[int] = None,
|
||||||
expires: datetime | None = None,
|
expires: Optional[datetime] = None,
|
||||||
httponly: bool = False,
|
httponly: bool = False,
|
||||||
samesite: SameSite | None = "Lax",
|
samesite: Optional[SameSite] = "Lax",
|
||||||
partitioned: bool = False,
|
partitioned: bool = False,
|
||||||
comment: str | None = None,
|
comment: Optional[str] = None,
|
||||||
host_prefix: bool = False,
|
host_prefix: bool = False,
|
||||||
secure_prefix: bool = False,
|
secure_prefix: bool = False,
|
||||||
) -> Cookie:
|
) -> Cookie:
|
||||||
|
@ -356,7 +362,7 @@ class CookieJar(dict):
|
||||||
key: str,
|
key: str,
|
||||||
*,
|
*,
|
||||||
path: str = "/",
|
path: str = "/",
|
||||||
domain: str | None = None,
|
domain: Optional[str] = None,
|
||||||
host_prefix: bool = False,
|
host_prefix: bool = False,
|
||||||
secure_prefix: bool = False,
|
secure_prefix: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -384,7 +390,7 @@ class CookieJar(dict):
|
||||||
:type secure_prefix: bool
|
:type secure_prefix: bool
|
||||||
"""
|
"""
|
||||||
# remove it from header
|
# remove it from header
|
||||||
cookies: list[Cookie] = self.headers.popall(self.HEADER_KEY, [])
|
cookies: List[Cookie] = self.headers.popall(self.HEADER_KEY, [])
|
||||||
for cookie in cookies:
|
for cookie in cookies:
|
||||||
if (
|
if (
|
||||||
cookie.key != Cookie.make_key(key, host_prefix, secure_prefix)
|
cookie.key != Cookie.make_key(key, host_prefix, secure_prefix)
|
||||||
|
@ -475,14 +481,14 @@ class Cookie(dict):
|
||||||
value: str,
|
value: str,
|
||||||
*,
|
*,
|
||||||
path: str = "/",
|
path: str = "/",
|
||||||
domain: str | None = None,
|
domain: Optional[str] = None,
|
||||||
secure: bool = True,
|
secure: bool = True,
|
||||||
max_age: int | None = None,
|
max_age: Optional[int] = None,
|
||||||
expires: datetime | None = None,
|
expires: Optional[datetime] = None,
|
||||||
httponly: bool = False,
|
httponly: bool = False,
|
||||||
samesite: SameSite | None = "Lax",
|
samesite: Optional[SameSite] = "Lax",
|
||||||
partitioned: bool = False,
|
partitioned: bool = False,
|
||||||
comment: str | None = None,
|
comment: Optional[str] = None,
|
||||||
host_prefix: bool = False,
|
host_prefix: bool = False,
|
||||||
secure_prefix: bool = False,
|
secure_prefix: bool = False,
|
||||||
):
|
):
|
||||||
|
@ -496,7 +502,9 @@ class Cookie(dict):
|
||||||
"Cannot set host_prefix on a cookie without secure=True"
|
"Cannot set host_prefix on a cookie without secure=True"
|
||||||
)
|
)
|
||||||
if path != "/":
|
if path != "/":
|
||||||
raise ServerError("Cannot set host_prefix on a cookie unless path='/'")
|
raise ServerError(
|
||||||
|
"Cannot set host_prefix on a cookie unless path='/'"
|
||||||
|
)
|
||||||
if domain:
|
if domain:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
"Cannot set host_prefix on a cookie with a defined domain"
|
"Cannot set host_prefix on a cookie with a defined domain"
|
||||||
|
@ -553,7 +561,7 @@ class Cookie(dict):
|
||||||
# in v24.3 when this is no longer a dict
|
# in v24.3 when this is no longer a dict
|
||||||
def _set_value(self, key: str, value: Any) -> None:
|
def _set_value(self, key: str, value: Any) -> None:
|
||||||
if key not in self._keys:
|
if key not in self._keys:
|
||||||
raise KeyError(f"Unknown cookie property: {key}={value}")
|
raise KeyError("Unknown cookie property: %s=%s" % (key, value))
|
||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
if key.lower() == "max-age" and not str(value).isdigit():
|
if key.lower() == "max-age" and not str(value).isdigit():
|
||||||
|
@ -596,18 +604,21 @@ class Cookie(dict):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Format as a Set-Cookie header value."""
|
"""Format as a Set-Cookie header value."""
|
||||||
output = [f"{self.key}={_quote(self.value)}"]
|
output = ["%s=%s" % (self.key, _quote(self.value))]
|
||||||
key_index = list(self._keys)
|
key_index = list(self._keys)
|
||||||
for key, value in sorted(self.items(), key=lambda x: key_index.index(x[0])):
|
for key, value in sorted(
|
||||||
|
self.items(), key=lambda x: key_index.index(x[0])
|
||||||
|
):
|
||||||
if value is not None and value is not False:
|
if value is not None and value is not False:
|
||||||
if key == "max-age":
|
if key == "max-age":
|
||||||
try:
|
try:
|
||||||
output.append("%s=%d" % (self._keys[key], value))
|
output.append("%s=%d" % (self._keys[key], value))
|
||||||
except TypeError:
|
except TypeError:
|
||||||
output.append(f"{self._keys[key]}={value}")
|
output.append("%s=%s" % (self._keys[key], value))
|
||||||
elif key == "expires":
|
elif key == "expires":
|
||||||
output.append(
|
output.append(
|
||||||
"{}={}".format(
|
"%s=%s"
|
||||||
|
% (
|
||||||
self._keys[key],
|
self._keys[key],
|
||||||
value.strftime("%a, %d-%b-%Y %T GMT"),
|
value.strftime("%a, %d-%b-%Y %T GMT"),
|
||||||
)
|
)
|
||||||
|
@ -615,7 +626,7 @@ class Cookie(dict):
|
||||||
elif key in self._flags:
|
elif key in self._flags:
|
||||||
output.append(self._keys[key])
|
output.append(self._keys[key])
|
||||||
else:
|
else:
|
||||||
output.append(f"{self._keys[key]}={value}")
|
output.append("%s=%s" % (self._keys[key], value))
|
||||||
|
|
||||||
return "; ".join(output)
|
return "; ".join(output)
|
||||||
|
|
||||||
|
@ -629,7 +640,7 @@ class Cookie(dict):
|
||||||
self._set_value("path", value)
|
self._set_value("path", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expires(self) -> datetime | None: # no cov
|
def expires(self) -> Optional[datetime]: # no cov
|
||||||
"""The expiration date of the cookie. Defaults to `None`."""
|
"""The expiration date of the cookie. Defaults to `None`."""
|
||||||
return self.get("expires")
|
return self.get("expires")
|
||||||
|
|
||||||
|
@ -638,7 +649,7 @@ class Cookie(dict):
|
||||||
self._set_value("expires", value)
|
self._set_value("expires", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def comment(self) -> str | None: # no cov
|
def comment(self) -> Optional[str]: # no cov
|
||||||
"""A comment for the cookie. Defaults to `None`."""
|
"""A comment for the cookie. Defaults to `None`."""
|
||||||
return self.get("comment")
|
return self.get("comment")
|
||||||
|
|
||||||
|
@ -647,7 +658,7 @@ class Cookie(dict):
|
||||||
self._set_value("comment", value)
|
self._set_value("comment", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def domain(self) -> str | None: # no cov
|
def domain(self) -> Optional[str]: # no cov
|
||||||
"""The domain of the cookie. Defaults to `None`."""
|
"""The domain of the cookie. Defaults to `None`."""
|
||||||
return self.get("domain")
|
return self.get("domain")
|
||||||
|
|
||||||
|
@ -656,7 +667,7 @@ class Cookie(dict):
|
||||||
self._set_value("domain", value)
|
self._set_value("domain", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_age(self) -> int | None: # no cov
|
def max_age(self) -> Optional[int]: # no cov
|
||||||
"""The maximum age of the cookie in seconds. Defaults to `None`."""
|
"""The maximum age of the cookie in seconds. Defaults to `None`."""
|
||||||
return self.get("max-age")
|
return self.get("max-age")
|
||||||
|
|
||||||
|
@ -683,7 +694,7 @@ class Cookie(dict):
|
||||||
self._set_value("httponly", value)
|
self._set_value("httponly", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def samesite(self) -> SameSite | None: # no cov
|
def samesite(self) -> Optional[SameSite]: # no cov
|
||||||
"""The SameSite attribute for the cookie. Defaults to `"Lax"`."""
|
"""The SameSite attribute for the cookie. Defaults to `"Lax"`."""
|
||||||
return self.get("samesite")
|
return self.get("samesite")
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from traceback import extract_tb
|
from traceback import extract_tb
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ from sanic.log import deprecation, logger
|
||||||
from sanic.pages.error import ErrorPage
|
from sanic.pages.error import ErrorPage
|
||||||
from sanic.response import html, json, text
|
from sanic.response import html, json, text
|
||||||
|
|
||||||
|
|
||||||
dumps: t.Callable[..., str]
|
dumps: t.Callable[..., str]
|
||||||
try:
|
try:
|
||||||
from ujson import dumps
|
from ujson import dumps
|
||||||
|
@ -71,7 +73,7 @@ class BaseRenderer:
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self) -> dict[str, str]:
|
def headers(self) -> t.Dict[str, str]:
|
||||||
"""The headers to be used for the response."""
|
"""The headers to be used for the response."""
|
||||||
if isinstance(self.exception, SanicException):
|
if isinstance(self.exception, SanicException):
|
||||||
return getattr(self.exception, "headers", {})
|
return getattr(self.exception, "headers", {})
|
||||||
|
@ -190,7 +192,8 @@ class TextRenderer(BaseRenderer):
|
||||||
lines += [
|
lines += [
|
||||||
f"{self.exception.__class__.__name__}: {self.exception} while "
|
f"{self.exception.__class__.__name__}: {self.exception} while "
|
||||||
f"handling path {self.request.path}",
|
f"handling path {self.request.path}",
|
||||||
f"Traceback of {self.request.app.name} " "(most recent call last):\n",
|
f"Traceback of {self.request.app.name} "
|
||||||
|
"(most recent call last):\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
while exc_value:
|
while exc_value:
|
||||||
|
@ -323,8 +326,8 @@ def exception_response(
|
||||||
exception: Exception,
|
exception: Exception,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
fallback: str,
|
fallback: str,
|
||||||
base: type[BaseRenderer],
|
base: t.Type[BaseRenderer],
|
||||||
renderer: type[BaseRenderer] | None = None,
|
renderer: t.Optional[t.Type[BaseRenderer]] = None,
|
||||||
) -> HTTPResponse:
|
) -> HTTPResponse:
|
||||||
"""Render a response for the default FALLBACK exception handler."""
|
"""Render a response for the default FALLBACK exception handler."""
|
||||||
if not renderer:
|
if not renderer:
|
||||||
|
@ -387,7 +390,9 @@ def guess_mime(req: Request, fallback: str) -> str:
|
||||||
if m:
|
if m:
|
||||||
format = CONFIG_BY_MIME[m.mime]
|
format = CONFIG_BY_MIME[m.mime]
|
||||||
source = formats[format]
|
source = formats[format]
|
||||||
logger.debug(f"The client accepts {m.header}, using '{format}' from {source}")
|
logger.debug(
|
||||||
|
f"The client accepts {m.header}, using '{format}' from {source}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"No format found, the client accepts {req.accept!r}")
|
logger.debug(f"No format found, the client accepts {req.accept!r}")
|
||||||
return m.mime
|
return m.mime
|
||||||
|
|
|
@ -69,7 +69,9 @@ class SanicException(Exception):
|
||||||
) -> None:
|
) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
self.extra = extra
|
self.extra = extra
|
||||||
status_code = status_code or getattr(self.__class__, "status_code", None)
|
status_code = status_code or getattr(
|
||||||
|
self.__class__, "status_code", None
|
||||||
|
)
|
||||||
quiet = quiet or getattr(self.__class__, "quiet", None)
|
quiet = quiet or getattr(self.__class__, "quiet", None)
|
||||||
headers = headers or getattr(self.__class__, "headers", {})
|
headers = headers or getattr(self.__class__, "headers", {})
|
||||||
if message is None:
|
if message is None:
|
||||||
|
@ -619,7 +621,9 @@ class Unauthorized(HTTPException):
|
||||||
|
|
||||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||||
if scheme is not None:
|
if scheme is not None:
|
||||||
values = [f'{k!s}="{v!s}"' for k, v in challenges.items()]
|
values = [
|
||||||
|
'{!s}="{!s}"'.format(k, v) for k, v in challenges.items()
|
||||||
|
]
|
||||||
challenge = ", ".join(values)
|
challenge = ", ".join(values)
|
||||||
|
|
||||||
self.headers = {
|
self.headers = {
|
||||||
|
|
|
@ -2,6 +2,7 @@ from .content_range import ContentRangeHandler
|
||||||
from .directory import DirectoryHandler
|
from .directory import DirectoryHandler
|
||||||
from .error import ErrorHandler
|
from .error import ErrorHandler
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"ContentRangeHandler",
|
"ContentRangeHandler",
|
||||||
"DirectoryHandler",
|
"DirectoryHandler",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
|
@ -10,6 +11,7 @@ from sanic.exceptions import (
|
||||||
)
|
)
|
||||||
from sanic.models.protocol_types import Range
|
from sanic.models.protocol_types import Range
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Request
|
from sanic import Request
|
||||||
|
|
||||||
|
@ -31,19 +33,27 @@ class ContentRangeHandler(Range):
|
||||||
raise HeaderNotFound("Range Header Not Found")
|
raise HeaderNotFound("Range Header Not Found")
|
||||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||||
if unit != "bytes":
|
if unit != "bytes":
|
||||||
raise InvalidRangeType(f"{unit} is not a valid Range Type", self)
|
raise InvalidRangeType(
|
||||||
|
"%s is not a valid Range Type" % (unit,), self
|
||||||
|
)
|
||||||
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
|
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
|
||||||
try:
|
try:
|
||||||
self.start = int(start_b) if start_b else None
|
self.start = int(start_b) if start_b else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise RangeNotSatisfiable(f"'{start_b}' is invalid for Content Range", self)
|
raise RangeNotSatisfiable(
|
||||||
|
"'%s' is invalid for Content Range" % (start_b,), self
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self.end = int(end_b) if end_b else None
|
self.end = int(end_b) if end_b else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise RangeNotSatisfiable(f"'{end_b}' is invalid for Content Range", self)
|
raise RangeNotSatisfiable(
|
||||||
|
"'%s' is invalid for Content Range" % (end_b,), self
|
||||||
|
)
|
||||||
if self.end is None:
|
if self.end is None:
|
||||||
if self.start is None:
|
if self.start is None:
|
||||||
raise RangeNotSatisfiable("Invalid for Content Range parameters", self)
|
raise RangeNotSatisfiable(
|
||||||
|
"Invalid for Content Range parameters", self
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# this case represents `Content-Range: bytes 5-`
|
# this case represents `Content-Range: bytes 5-`
|
||||||
self.end = self.total - 1
|
self.end = self.total - 1
|
||||||
|
@ -53,9 +63,14 @@ class ContentRangeHandler(Range):
|
||||||
self.start = self.total - self.end
|
self.start = self.total - self.end
|
||||||
self.end = self.total - 1
|
self.end = self.total - 1
|
||||||
if self.start >= self.end:
|
if self.start >= self.end:
|
||||||
raise RangeNotSatisfiable("Invalid for Content Range parameters", self)
|
raise RangeNotSatisfiable(
|
||||||
|
"Invalid for Content Range parameters", self
|
||||||
|
)
|
||||||
self.size = self.end - self.start + 1
|
self.size = self.end - self.start + 1
|
||||||
self.headers = {"Content-Range": f"bytes {self.start}-{self.end}/{self.total}"}
|
self.headers = {
|
||||||
|
"Content-Range": "bytes %s-%s/%s"
|
||||||
|
% (self.start, self.end, self.total)
|
||||||
|
}
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return hasattr(self, "size") and self.size > 0
|
return hasattr(self, "size") and self.size > 0
|
||||||
|
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from stat import S_ISDIR
|
from stat import S_ISDIR
|
||||||
from typing import Iterable, Sequence, cast
|
from typing import Dict, Iterable, Optional, Sequence, Union, cast
|
||||||
|
|
||||||
from sanic.exceptions import NotFound
|
from sanic.exceptions import NotFound
|
||||||
from sanic.pages.directory_page import DirectoryPage, FileInfo
|
from sanic.pages.directory_page import DirectoryPage, FileInfo
|
||||||
|
@ -28,7 +28,7 @@ class DirectoryHandler:
|
||||||
uri: str,
|
uri: str,
|
||||||
directory: Path,
|
directory: Path,
|
||||||
directory_view: bool = False,
|
directory_view: bool = False,
|
||||||
index: str | Sequence[str] | None = None,
|
index: Optional[Union[str, Sequence[str]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(index, str):
|
if isinstance(index, str):
|
||||||
index = [index]
|
index = [index]
|
||||||
|
@ -60,7 +60,9 @@ class DirectoryHandler:
|
||||||
return await file(index_file)
|
return await file(index_file)
|
||||||
|
|
||||||
if self.directory_view:
|
if self.directory_view:
|
||||||
return self._index(self.directory / current, path, request.app.debug)
|
return self._index(
|
||||||
|
self.directory / current, path, request.app.debug
|
||||||
|
)
|
||||||
|
|
||||||
if self.index:
|
if self.index:
|
||||||
raise NotFound("File not found")
|
raise NotFound("File not found")
|
||||||
|
@ -70,16 +72,20 @@ class DirectoryHandler:
|
||||||
def _index(self, location: Path, path: str, debug: bool):
|
def _index(self, location: Path, path: str, debug: bool):
|
||||||
# Remove empty path elements, append slash
|
# Remove empty path elements, append slash
|
||||||
if "//" in path or not path.endswith("/"):
|
if "//" in path or not path.endswith("/"):
|
||||||
return redirect("/" + "".join([f"{p}/" for p in path.split("/") if p]))
|
return redirect(
|
||||||
|
"/" + "".join([f"{p}/" for p in path.split("/") if p])
|
||||||
|
)
|
||||||
|
|
||||||
# Render file browser
|
# Render file browser
|
||||||
page = DirectoryPage(self._iter_files(location), path, debug)
|
page = DirectoryPage(self._iter_files(location), path, debug)
|
||||||
return html(page.render())
|
return html(page.render())
|
||||||
|
|
||||||
def _prepare_file(self, path: Path) -> dict[str, int | str]:
|
def _prepare_file(self, path: Path) -> Dict[str, Union[int, str]]:
|
||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
modified = (
|
modified = (
|
||||||
datetime.fromtimestamp(stat.st_mtime).isoformat()[:19].replace("T", " ")
|
datetime.fromtimestamp(stat.st_mtime)
|
||||||
|
.isoformat()[:19]
|
||||||
|
.replace("T", " ")
|
||||||
)
|
)
|
||||||
is_dir = S_ISDIR(stat.st_mode)
|
is_dir = S_ISDIR(stat.st_mode)
|
||||||
icon = "📁" if is_dir else "📄"
|
icon = "📁" if is_dir else "📄"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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 ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.log import error_logger
|
from sanic.log import error_logger
|
||||||
|
@ -23,20 +25,20 @@ class ErrorHandler:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
base: type[BaseRenderer] = TextRenderer,
|
base: Type[BaseRenderer] = TextRenderer,
|
||||||
):
|
):
|
||||||
self.cached_handlers: dict[
|
self.cached_handlers: Dict[
|
||||||
tuple[type[BaseException], str | None], RouteHandler | None
|
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
|
||||||
] = {}
|
] = {}
|
||||||
self.debug = False
|
self.debug = False
|
||||||
self.base = base
|
self.base = base
|
||||||
|
|
||||||
def _full_lookup(self, exception, route_name: str | None = 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(
|
def _add(
|
||||||
self,
|
self,
|
||||||
key: tuple[type[BaseException], str | None],
|
key: Tuple[Type[BaseException], Optional[str]],
|
||||||
handler: RouteHandler,
|
handler: RouteHandler,
|
||||||
) -> None:
|
) -> None:
|
||||||
if key in self.cached_handlers:
|
if key in self.cached_handlers:
|
||||||
|
@ -51,7 +53,7 @@ class ErrorHandler:
|
||||||
raise ServerError(message)
|
raise ServerError(message)
|
||||||
self.cached_handlers[key] = handler
|
self.cached_handlers[key] = handler
|
||||||
|
|
||||||
def add(self, exception, handler, route_names: list[str] | None = 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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -70,7 +72,7 @@ class ErrorHandler:
|
||||||
else:
|
else:
|
||||||
self._add((exception, None), handler)
|
self._add((exception, None), handler)
|
||||||
|
|
||||||
def lookup(self, exception, route_name: str | None = None):
|
def lookup(self, exception, route_name: Optional[str] = None):
|
||||||
"""Lookup the existing instance of `ErrorHandler` and fetch the registered handler for a specific type of exception.
|
"""Lookup the existing instance of `ErrorHandler` and fetch the registered handler for a specific type of exception.
|
||||||
|
|
||||||
This method leverages a dict lookup to speedup the retrieval process.
|
This method leverages a dict lookup to speedup the retrieval process.
|
||||||
|
@ -96,7 +98,9 @@ class ErrorHandler:
|
||||||
exception_key = (ancestor, name)
|
exception_key = (ancestor, name)
|
||||||
if exception_key in self.cached_handlers:
|
if exception_key in self.cached_handlers:
|
||||||
handler = self.cached_handlers[exception_key]
|
handler = self.cached_handlers[exception_key]
|
||||||
self.cached_handlers[(exception_class, route_name)] = handler
|
self.cached_handlers[
|
||||||
|
(exception_class, route_name)
|
||||||
|
] = handler
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
if ancestor is BaseException:
|
if ancestor is BaseException:
|
||||||
|
@ -131,11 +135,13 @@ class ErrorHandler:
|
||||||
url = repr(request.url)
|
url = repr(request.url)
|
||||||
except AttributeError: # no cov
|
except AttributeError: # no cov
|
||||||
url = "unknown"
|
url = "unknown"
|
||||||
response_message = f'Exception raised in exception handler "{handler.__name__}" for uri: {url}'
|
response_message = (
|
||||||
error_logger.exception(response_message)
|
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||||
|
)
|
||||||
|
error_logger.exception(response_message, handler.__name__, url)
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
return text(response_message, 500)
|
return text(response_message % (handler.__name__, url), 500)
|
||||||
else:
|
else:
|
||||||
return text("An error occurred while handling an error", 500)
|
return text("An error occurred while handling an error", 500)
|
||||||
return response
|
return response
|
||||||
|
@ -194,4 +200,6 @@ class ErrorHandler:
|
||||||
except AttributeError: # no cov
|
except AttributeError: # no cov
|
||||||
url = "unknown"
|
url = "unknown"
|
||||||
|
|
||||||
error_logger.exception("Exception occurred while handling uri: %s", url)
|
error_logger.exception(
|
||||||
|
"Exception occurred while handling uri: %s", url
|
||||||
|
)
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, Iterable, Tuple, Union
|
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from sanic.exceptions import InvalidHeader
|
from sanic.exceptions import InvalidHeader
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
# - the Options object should be a typed object to allow for less casting
|
# - the Options object should be a typed object to allow for less casting
|
||||||
# across the application (in request.py for example)
|
# across the application (in request.py for example)
|
||||||
|
@ -19,7 +21,9 @@ _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
||||||
_param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
_param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
||||||
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
|
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
|
||||||
_ipv6_re = re.compile(_ipv6)
|
_ipv6_re = re.compile(_ipv6)
|
||||||
_host_re = re.compile(r"((?:\[" + _ipv6 + r"\])|[a-zA-Z0-9.\-]{1,253})(?::(\d{1,5}))?")
|
_host_re = re.compile(
|
||||||
|
r"((?:\[" + _ipv6 + r"\])|[a-zA-Z0-9.\-]{1,253})(?::(\d{1,5}))?"
|
||||||
|
)
|
||||||
|
|
||||||
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
|
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
|
||||||
# curl all have different escaping, that we try to handle as well as possible,
|
# curl all have different escaping, that we try to handle as well as possible,
|
||||||
|
@ -81,8 +85,8 @@ class MediaType:
|
||||||
|
|
||||||
def match(
|
def match(
|
||||||
self,
|
self,
|
||||||
mime_with_params: str | MediaType,
|
mime_with_params: Union[str, MediaType],
|
||||||
) -> MediaType | None:
|
) -> Optional[MediaType]:
|
||||||
"""Match this media type against another media type.
|
"""Match this media type against another media type.
|
||||||
|
|
||||||
Check if this media type matches the given mime type/subtype.
|
Check if this media type matches the given mime type/subtype.
|
||||||
|
@ -120,7 +124,9 @@ class MediaType:
|
||||||
or mt.subtype == "*"
|
or mt.subtype == "*"
|
||||||
)
|
)
|
||||||
# Type match
|
# Type match
|
||||||
and (self.type == mt.type or self.type == "*" or mt.type == "*")
|
and (
|
||||||
|
self.type == mt.type or self.type == "*" or mt.type == "*"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
@ -135,7 +141,7 @@ class MediaType:
|
||||||
return any(part == "*" for part in (self.subtype, self.type))
|
return any(part == "*" for part in (self.subtype, self.type))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse(cls, mime_with_params: str) -> MediaType | None:
|
def _parse(cls, mime_with_params: str) -> Optional[MediaType]:
|
||||||
mtype = mime_with_params.strip()
|
mtype = mime_with_params.strip()
|
||||||
if "/" not in mime_with_params:
|
if "/" not in mime_with_params:
|
||||||
return None
|
return None
|
||||||
|
@ -145,10 +151,12 @@ class MediaType:
|
||||||
if not type_ or not subtype:
|
if not type_ or not subtype:
|
||||||
raise ValueError(f"Invalid media type: {mtype}")
|
raise ValueError(f"Invalid media type: {mtype}")
|
||||||
|
|
||||||
params = {
|
params = dict(
|
||||||
key.strip(): value.strip()
|
[
|
||||||
|
(key.strip(), value.strip())
|
||||||
for key, value in (param.split("=", 1) for param in raw_params)
|
for key, value in (param.split("=", 1) for param in raw_params)
|
||||||
}
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return cls(type_.lstrip(), subtype.rstrip(), **params)
|
return cls(type_.lstrip(), subtype.rstrip(), **params)
|
||||||
|
|
||||||
|
@ -165,7 +173,7 @@ class Matched:
|
||||||
header (MediaType): The header to match against, if any.
|
header (MediaType): The header to match against, if any.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mime: str, header: MediaType | None):
|
def __init__(self, mime: str, header: Optional[MediaType]):
|
||||||
self.mime = mime
|
self.mime = mime
|
||||||
self.header = header
|
self.header = header
|
||||||
|
|
||||||
|
@ -192,7 +200,7 @@ class Matched:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _compare(self, other) -> tuple[bool, Matched]:
|
def _compare(self, other) -> Tuple[bool, Matched]:
|
||||||
if isinstance(other, str):
|
if isinstance(other, str):
|
||||||
parsed = Matched.parse(other)
|
parsed = Matched.parse(other)
|
||||||
if self.mime == other:
|
if self.mime == other:
|
||||||
|
@ -207,7 +215,7 @@ class Matched:
|
||||||
f"mime types of '{self.mime}' and '{other}'"
|
f"mime types of '{self.mime}' and '{other}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
def match(self, other: str | Matched) -> Matched | None:
|
def match(self, other: Union[str, Matched]) -> Optional[Matched]:
|
||||||
"""Match this MIME string against another MIME string.
|
"""Match this MIME string against another MIME string.
|
||||||
|
|
||||||
Check if this MIME string matches the given MIME string. Wildcards are supported both ways on both type and subtype.
|
Check if this MIME string matches the given MIME string. Wildcards are supported both ways on both type and subtype.
|
||||||
|
@ -288,7 +296,7 @@ class AcceptList(list):
|
||||||
return ", ".join(str(m) for m in self)
|
return ", ".join(str(m) for m in self)
|
||||||
|
|
||||||
|
|
||||||
def parse_accept(accept: str | None) -> AcceptList:
|
def parse_accept(accept: Optional[str]) -> AcceptList:
|
||||||
"""Parse an Accept header and order the acceptable media types according to RFC 7231, s. 5.3.2
|
"""Parse an Accept header and order the acceptable media types according to RFC 7231, s. 5.3.2
|
||||||
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||||
|
@ -308,7 +316,9 @@ def parse_accept(accept: str | None) -> AcceptList:
|
||||||
accept = "*/*" # No header means that all types are accepted
|
accept = "*/*" # No header means that all types are accepted
|
||||||
try:
|
try:
|
||||||
a = [
|
a = [
|
||||||
mt for mt in [MediaType._parse(mtype) for mtype in accept.split(",")] if mt
|
mt
|
||||||
|
for mt in [MediaType._parse(mtype) for mtype in accept.split(",")]
|
||||||
|
if mt
|
||||||
]
|
]
|
||||||
if not a:
|
if not a:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
@ -317,7 +327,7 @@ def parse_accept(accept: str | None) -> AcceptList:
|
||||||
raise InvalidHeader(f"Invalid header value in Accept: {accept}")
|
raise InvalidHeader(f"Invalid header value in Accept: {accept}")
|
||||||
|
|
||||||
|
|
||||||
def parse_content_header(value: str) -> tuple[str, Options]:
|
def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||||
"""Parse content-type and content-disposition header values.
|
"""Parse content-type and content-disposition header values.
|
||||||
|
|
||||||
E.g. `form-data; name=upload; filename="file.txt"` to
|
E.g. `form-data; name=upload; filename="file.txt"` to
|
||||||
|
@ -336,10 +346,11 @@ def parse_content_header(value: str) -> tuple[str, Options]:
|
||||||
"""
|
"""
|
||||||
pos = value.find(";")
|
pos = value.find(";")
|
||||||
if pos == -1:
|
if pos == -1:
|
||||||
options: dict[str, int | str] = {}
|
options: Dict[str, Union[int, str]] = {}
|
||||||
else:
|
else:
|
||||||
options = {
|
options = {
|
||||||
m.group(1).lower(): (m.group(2) or m.group(3))
|
m.group(1)
|
||||||
|
.lower(): (m.group(2) or m.group(3))
|
||||||
.replace("%22", '"')
|
.replace("%22", '"')
|
||||||
.replace("%0D%0A", "\n")
|
.replace("%0D%0A", "\n")
|
||||||
for m in _param.finditer(value[pos:])
|
for m in _param.finditer(value[pos:])
|
||||||
|
@ -356,7 +367,7 @@ def parse_content_header(value: str) -> tuple[str, Options]:
|
||||||
_rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII)
|
_rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII)
|
||||||
|
|
||||||
|
|
||||||
def parse_forwarded(headers, config) -> Options | None:
|
def parse_forwarded(headers, config) -> Optional[Options]:
|
||||||
"""Parse RFC 7239 Forwarded headers.
|
"""Parse RFC 7239 Forwarded headers.
|
||||||
The value of `by` or `secret` must match `config.FORWARDED_SECRET`
|
The value of `by` or `secret` must match `config.FORWARDED_SECRET`
|
||||||
:return: dict with keys and values, or None if nothing matched
|
:return: dict with keys and values, or None if nothing matched
|
||||||
|
@ -370,7 +381,7 @@ def parse_forwarded(headers, config) -> Options | None:
|
||||||
return None
|
return None
|
||||||
# Loop over <separator><key>=<value> elements from right to left
|
# Loop over <separator><key>=<value> elements from right to left
|
||||||
sep = pos = None
|
sep = pos = None
|
||||||
options: list[tuple[str, str]] = []
|
options: List[Tuple[str, str]] = []
|
||||||
found = False
|
found = False
|
||||||
for m in _rparam.finditer(header[::-1]):
|
for m in _rparam.finditer(header[::-1]):
|
||||||
# Start of new element? (on parser skips and non-semicolon right sep)
|
# Start of new element? (on parser skips and non-semicolon right sep)
|
||||||
|
@ -394,7 +405,7 @@ def parse_forwarded(headers, config) -> Options | None:
|
||||||
return fwd_normalize(reversed(options)) if found else None
|
return fwd_normalize(reversed(options)) if found else None
|
||||||
|
|
||||||
|
|
||||||
def parse_xforwarded(headers, config) -> Options | None:
|
def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||||
"""Parse traditional proxy headers."""
|
"""Parse traditional proxy headers."""
|
||||||
real_ip_header = config.REAL_IP_HEADER
|
real_ip_header = config.REAL_IP_HEADER
|
||||||
proxies_count = config.PROXIES_COUNT
|
proxies_count = config.PROXIES_COUNT
|
||||||
|
@ -405,7 +416,11 @@ def parse_xforwarded(headers, config) -> Options | None:
|
||||||
# Combine, split and filter multiple headers' entries
|
# Combine, split and filter multiple headers' entries
|
||||||
forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER)
|
forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER)
|
||||||
proxies = [
|
proxies = [
|
||||||
p for p in (p.strip() for h in forwarded_for for p in h.split(",")) if p
|
p
|
||||||
|
for p in (
|
||||||
|
p.strip() for h in forwarded_for for p in h.split(",")
|
||||||
|
)
|
||||||
|
if p
|
||||||
]
|
]
|
||||||
addr = proxies[-proxies_count]
|
addr = proxies[-proxies_count]
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
|
@ -437,7 +452,7 @@ def fwd_normalize(fwd: OptionsIterable) -> Options:
|
||||||
Returns:
|
Returns:
|
||||||
Options: A dict of normalized key-value pairs.
|
Options: A dict of normalized key-value pairs.
|
||||||
"""
|
"""
|
||||||
ret: dict[str, int | str] = {}
|
ret: Dict[str, Union[int, str]] = {}
|
||||||
for key, val in fwd:
|
for key, val in fwd:
|
||||||
if val is not None:
|
if val is not None:
|
||||||
try:
|
try:
|
||||||
|
@ -474,7 +489,7 @@ def fwd_normalize_address(addr: str) -> str:
|
||||||
return addr.lower()
|
return addr.lower()
|
||||||
|
|
||||||
|
|
||||||
def parse_host(host: str) -> tuple[str | None, int | None]:
|
def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
|
||||||
"""Split host:port into hostname and port.
|
"""Split host:port into hostname and port.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -516,9 +531,9 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
|
||||||
|
|
||||||
|
|
||||||
def parse_credentials(
|
def parse_credentials(
|
||||||
header: str | None,
|
header: Optional[str],
|
||||||
prefixes: list | tuple | set | None = None,
|
prefixes: Optional[Union[List, Tuple, Set]] = None,
|
||||||
) -> tuple[str | None, str | None]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""Parses any header with the aim to retrieve any credentials from it.
|
"""Parses any header with the aim to retrieve any credentials from it.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
"""Defines basics of HTTP standard."""
|
"""Defines basics of HTTP standard."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from inspect import ismodule
|
from inspect import ismodule
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
STATUS_CODES: Dict[int, bytes] = {
|
STATUS_CODES: Dict[int, bytes] = {
|
||||||
100: b"Continue",
|
100: b"Continue",
|
||||||
101: b"Switching Protocols",
|
101: b"Switching Protocols",
|
||||||
|
@ -130,7 +132,7 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
||||||
|
|
||||||
returns the headers without the entity headers
|
returns the headers without the entity headers
|
||||||
"""
|
"""
|
||||||
allowed = {h.lower() for h in allowed}
|
allowed = set([h.lower() for h in allowed])
|
||||||
headers = {
|
headers = {
|
||||||
header: value
|
header: value
|
||||||
for header, value in headers.items()
|
for header, value in headers.items()
|
||||||
|
|
|
@ -2,4 +2,5 @@ from .constants import Stage
|
||||||
from .http1 import Http
|
from .http1 import Http
|
||||||
from .http3 import Http3
|
from .http3 import Http3
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("Http", "Stage", "Http3")
|
__all__ = ("Http", "Stage", "Http3")
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
|
@ -24,6 +25,7 @@ from sanic.http.stream import Stream
|
||||||
from sanic.log import access_logger, error_logger, logger
|
from sanic.log import access_logger, error_logger, logger
|
||||||
from sanic.touchup import TouchUpMeta
|
from sanic.touchup import TouchUpMeta
|
||||||
|
|
||||||
|
|
||||||
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
||||||
|
|
||||||
|
|
||||||
|
@ -361,20 +363,26 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||||
self.response_func = None
|
self.response_func = None
|
||||||
self.stage = Stage.IDLE
|
self.stage = Stage.IDLE
|
||||||
|
|
||||||
async def http1_response_chunked(self, data: bytes, end_stream: bool) -> None:
|
async def http1_response_chunked(
|
||||||
|
self, data: bytes, end_stream: bool
|
||||||
|
) -> None:
|
||||||
"""Format a part of response body in chunked encoding."""
|
"""Format a part of response body in chunked encoding."""
|
||||||
# Chunked encoding
|
# Chunked encoding
|
||||||
size = len(data)
|
size = len(data)
|
||||||
if end_stream:
|
if end_stream:
|
||||||
await self._send(
|
await self._send(
|
||||||
b"%x\r\n%b\r\n0\r\n\r\n" % (size, data) if size else b"0\r\n\r\n"
|
b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
|
||||||
|
if size
|
||||||
|
else b"0\r\n\r\n"
|
||||||
)
|
)
|
||||||
self.response_func = None
|
self.response_func = None
|
||||||
self.stage = Stage.IDLE
|
self.stage = Stage.IDLE
|
||||||
elif size:
|
elif size:
|
||||||
await self._send(b"%x\r\n%b\r\n" % (size, data))
|
await self._send(b"%x\r\n%b\r\n" % (size, data))
|
||||||
|
|
||||||
async def http1_response_normal(self, data: bytes, end_stream: bool) -> None:
|
async def http1_response_normal(
|
||||||
|
self, data: bytes, end_stream: bool
|
||||||
|
) -> None:
|
||||||
"""Format / keep track of non-chunked response."""
|
"""Format / keep track of non-chunked response."""
|
||||||
bytes_left = self.response_bytes_left - len(data)
|
bytes_left = self.response_bytes_left - len(data)
|
||||||
if bytes_left <= 0:
|
if bytes_left <= 0:
|
||||||
|
@ -412,7 +420,9 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||||
exception, (ServiceUnavailable, RequestCancelled)
|
exception, (ServiceUnavailable, RequestCancelled)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await app.handle_exception(self.request, exception, request_middleware)
|
await app.handle_exception(
|
||||||
|
self.request, exception, request_middleware
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await app.handle_exception(self.request, e, False)
|
await app.handle_exception(self.request, e, False)
|
||||||
|
|
||||||
|
@ -471,7 +481,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||||
if data:
|
if data:
|
||||||
yield data
|
yield data
|
||||||
|
|
||||||
async def read(self) -> bytes | None: # no cov
|
async def read(self) -> Optional[bytes]: # no cov
|
||||||
"""Read some bytes of request body."""
|
"""Read some bytes of request body."""
|
||||||
|
|
||||||
# Send a 100-continue if needed
|
# Send a 100-continue if needed
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from ssl import SSLContext
|
from ssl import SSLContext
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
@ -27,6 +32,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from aioquic.h0.connection import H0_ALPN, H0Connection
|
from aioquic.h0.connection import H0_ALPN, H0Connection
|
||||||
from aioquic.h3.connection import H3_ALPN, H3Connection
|
from aioquic.h3.connection import H3_ALPN, H3Connection
|
||||||
|
@ -65,7 +71,10 @@ class HTTP3Transport(TransportProtocol):
|
||||||
return self._protocol
|
return self._protocol
|
||||||
|
|
||||||
def get_extra_info(self, info: str, default: Any = None) -> Any:
|
def get_extra_info(self, info: str, default: Any = None) -> Any:
|
||||||
if info in ("socket", "sockname", "peername") and self._protocol._transport:
|
if (
|
||||||
|
info in ("socket", "sockname", "peername")
|
||||||
|
and self._protocol._transport
|
||||||
|
):
|
||||||
return self._protocol._transport.get_extra_info(info, default)
|
return self._protocol._transport.get_extra_info(info, default)
|
||||||
elif info == "network_paths":
|
elif info == "network_paths":
|
||||||
return self._protocol._quic._network_paths
|
return self._protocol._quic._network_paths
|
||||||
|
@ -100,18 +109,19 @@ class HTTPReceiver(Receiver, Stream):
|
||||||
self.request_body = None
|
self.request_body = None
|
||||||
self.stage = Stage.IDLE
|
self.stage = Stage.IDLE
|
||||||
self.headers_sent = False
|
self.headers_sent = False
|
||||||
self.response: BaseHTTPResponse | None = None
|
self.response: Optional[BaseHTTPResponse] = None
|
||||||
self.request_max_size = self.protocol.request_max_size
|
self.request_max_size = self.protocol.request_max_size
|
||||||
self.request_bytes = 0
|
self.request_bytes = 0
|
||||||
|
|
||||||
async def run(self, exception: Exception | None = None):
|
async def run(self, exception: Optional[Exception] = None):
|
||||||
"""Handle the request and response cycle."""
|
"""Handle the request and response cycle."""
|
||||||
self.stage = Stage.HANDLER
|
self.stage = Stage.HANDLER
|
||||||
self.head_only = self.request.method.upper() == "HEAD"
|
self.head_only = self.request.method.upper() == "HEAD"
|
||||||
|
|
||||||
if exception:
|
if exception:
|
||||||
logger.info( # no cov
|
logger.info( # no cov
|
||||||
f"{Colors.BLUE}[exception]: " f"{Colors.RED}{exception}{Colors.END}",
|
f"{Colors.BLUE}[exception]: "
|
||||||
|
f"{Colors.RED}{exception}{Colors.END}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
extra={"verbosity": 1},
|
extra={"verbosity": 1},
|
||||||
)
|
)
|
||||||
|
@ -136,13 +146,17 @@ class HTTPReceiver(Receiver, Stream):
|
||||||
|
|
||||||
await app.handle_exception(self.request, exception)
|
await app.handle_exception(self.request, exception)
|
||||||
|
|
||||||
def _prepare_headers(self, response: BaseHTTPResponse) -> list[tuple[bytes, bytes]]:
|
def _prepare_headers(
|
||||||
|
self, response: BaseHTTPResponse
|
||||||
|
) -> List[Tuple[bytes, bytes]]:
|
||||||
size = len(response.body) if response.body else 0
|
size = len(response.body) if response.body else 0
|
||||||
headers = response.headers
|
headers = response.headers
|
||||||
status = response.status
|
status = response.status
|
||||||
|
|
||||||
if not has_message_body(status) and (
|
if not has_message_body(status) and (
|
||||||
size or "content-length" in headers or "transfer-encoding" in headers
|
size
|
||||||
|
or "content-length" in headers
|
||||||
|
or "transfer-encoding" in headers
|
||||||
):
|
):
|
||||||
headers.pop("content-length", None)
|
headers.pop("content-length", None)
|
||||||
headers.pop("transfer-encoding", None)
|
headers.pop("transfer-encoding", None)
|
||||||
|
@ -235,7 +249,11 @@ class HTTPReceiver(Receiver, Stream):
|
||||||
):
|
):
|
||||||
size = len(data)
|
size = len(data)
|
||||||
if end_stream:
|
if end_stream:
|
||||||
data = b"%x\r\n%b\r\n0\r\n\r\n" % (size, data) if size else b"0\r\n\r\n"
|
data = (
|
||||||
|
b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
|
||||||
|
if size
|
||||||
|
else b"0\r\n\r\n"
|
||||||
|
)
|
||||||
elif size:
|
elif size:
|
||||||
data = b"%x\r\n%b\r\n" % (size, data)
|
data = b"%x\r\n%b\r\n" % (size, data)
|
||||||
|
|
||||||
|
@ -286,7 +304,7 @@ class Http3:
|
||||||
) -> None:
|
) -> None:
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.transmit = transmit
|
self.transmit = transmit
|
||||||
self.receivers: dict[int, Receiver] = {}
|
self.receivers: Dict[int, Receiver] = {}
|
||||||
|
|
||||||
def http_event_received(self, event: H3Event) -> None:
|
def http_event_received(self, event: H3Event) -> None:
|
||||||
logger.debug( # no cov
|
logger.debug( # no cov
|
||||||
|
@ -312,8 +330,11 @@ class Http3:
|
||||||
extra={"verbosity": 2},
|
extra={"verbosity": 2},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_or_make_receiver(self, event: H3Event) -> tuple[Receiver, bool]:
|
def get_or_make_receiver(self, event: H3Event) -> Tuple[Receiver, bool]:
|
||||||
if isinstance(event, HeadersReceived) and event.stream_id not in self.receivers:
|
if (
|
||||||
|
isinstance(event, HeadersReceived)
|
||||||
|
and event.stream_id not in self.receivers
|
||||||
|
):
|
||||||
request = self._make_request(event)
|
request = self._make_request(event)
|
||||||
receiver = HTTPReceiver(self.transmit, self.protocol, request)
|
receiver = HTTPReceiver(self.transmit, self.protocol, request)
|
||||||
request.stream = receiver
|
request.stream = receiver
|
||||||
|
@ -336,7 +357,9 @@ class Http3:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise BadRequest("Header names may only contain US-ASCII characters.")
|
raise BadRequest(
|
||||||
|
"Header names may only contain US-ASCII characters."
|
||||||
|
)
|
||||||
method = headers[":method"]
|
method = headers[":method"]
|
||||||
path = headers[":path"]
|
path = headers[":path"]
|
||||||
scheme = headers.pop(":scheme", "")
|
scheme = headers.pop(":scheme", "")
|
||||||
|
@ -373,16 +396,18 @@ class SessionTicketStore:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.tickets: dict[bytes, SessionTicket] = {}
|
self.tickets: Dict[bytes, SessionTicket] = {}
|
||||||
|
|
||||||
def add(self, ticket: SessionTicket) -> None:
|
def add(self, ticket: SessionTicket) -> None:
|
||||||
self.tickets[ticket.ticket] = ticket
|
self.tickets[ticket.ticket] = ticket
|
||||||
|
|
||||||
def pop(self, label: bytes) -> SessionTicket | None:
|
def pop(self, label: bytes) -> Optional[SessionTicket]:
|
||||||
return self.tickets.pop(label, None)
|
return self.tickets.pop(label, None)
|
||||||
|
|
||||||
|
|
||||||
def get_config(app: Sanic, ssl: SanicSSLContext | CertSelector | SSLContext):
|
def get_config(
|
||||||
|
app: Sanic, ssl: Union[SanicSSLContext, CertSelector, SSLContext]
|
||||||
|
):
|
||||||
# TODO:
|
# TODO:
|
||||||
# - proper selection needed if service with multiple certs insted of
|
# - proper selection needed if service with multiple certs insted of
|
||||||
# just taking the first
|
# just taking the first
|
||||||
|
@ -405,6 +430,8 @@ def get_config(app: Sanic, ssl: SanicSSLContext | CertSelector | SSLContext):
|
||||||
)
|
)
|
||||||
password = app.config.TLS_CERT_PASSWORD or None
|
password = app.config.TLS_CERT_PASSWORD or None
|
||||||
|
|
||||||
config.load_cert_chain(ssl.sanic["cert"], ssl.sanic["key"], password=password)
|
config.load_cert_chain(
|
||||||
|
ssl.sanic["cert"], ssl.sanic["key"], password=password
|
||||||
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||||
|
|
||||||
from sanic.http.constants import Stage
|
from sanic.http.constants import Stage
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.response import BaseHTTPResponse
|
from sanic.response import BaseHTTPResponse
|
||||||
from sanic.server.protocols.http_protocol import HttpProtocol
|
from sanic.server.protocols.http_protocol import HttpProtocol
|
||||||
|
@ -11,14 +12,16 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class Stream:
|
class Stream:
|
||||||
stage: Stage
|
stage: Stage
|
||||||
response: BaseHTTPResponse | None
|
response: Optional[BaseHTTPResponse]
|
||||||
protocol: HttpProtocol
|
protocol: HttpProtocol
|
||||||
url: str | None
|
url: Optional[str]
|
||||||
request_body: bytes | None
|
request_body: Optional[bytes]
|
||||||
request_max_size: int | float
|
request_max_size: Union[int, float]
|
||||||
|
|
||||||
__touchup__: tuple[str, ...] = ()
|
__touchup__: Tuple[str, ...] = tuple()
|
||||||
__slots__ = ("request_max_size",)
|
__slots__ = ("request_max_size",)
|
||||||
|
|
||||||
def respond(self, response: BaseHTTPResponse) -> BaseHTTPResponse: # no cov
|
def respond(
|
||||||
|
self, response: BaseHTTPResponse
|
||||||
|
) -> BaseHTTPResponse: # no cov
|
||||||
raise NotImplementedError("Not implemented")
|
raise NotImplementedError("Not implemented")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .context import process_to_context
|
from .context import process_to_context
|
||||||
from .creators import get_ssl_context
|
from .creators import get_ssl_context
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("get_ssl_context", "process_to_context")
|
__all__ = ("get_ssl_context", "process_to_context")
|
||||||
|
|
|
@ -2,10 +2,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import ssl
|
import ssl
|
||||||
from typing import Any, Iterable
|
|
||||||
|
from typing import Any, Dict, Iterable, Optional, Union
|
||||||
|
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
|
|
||||||
|
|
||||||
# Only allow secure ciphers, notably leaving out AES-CBC mode
|
# Only allow secure ciphers, notably leaving out AES-CBC mode
|
||||||
# OpenSSL chooses ECDSA or RSA depending on the cert in use
|
# OpenSSL chooses ECDSA or RSA depending on the cert in use
|
||||||
CIPHERS_TLS12 = [
|
CIPHERS_TLS12 = [
|
||||||
|
@ -17,14 +19,11 @@ CIPHERS_TLS12 = [
|
||||||
"ECDHE-RSA-AES128-GCM-SHA256",
|
"ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
]
|
]
|
||||||
|
|
||||||
TlsDef = None | ssl.SSLContext | dict[str, Any] | str
|
|
||||||
TlsDefs = TlsDef | list[TlsDef] | tuple[TlsDef, ...]
|
|
||||||
|
|
||||||
|
|
||||||
def create_context(
|
def create_context(
|
||||||
certfile: str | None = None,
|
certfile: Optional[str] = None,
|
||||||
keyfile: str | None = None,
|
keyfile: Optional[str] = None,
|
||||||
password: str | None = None,
|
password: Optional[str] = None,
|
||||||
purpose: ssl.Purpose = ssl.Purpose.CLIENT_AUTH,
|
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."""
|
||||||
|
@ -39,7 +38,9 @@ def create_context(
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def shorthand_to_ctx(ctxdef: TlsDef) -> ssl.SSLContext | None:
|
def shorthand_to_ctx(
|
||||||
|
ctxdef: Union[None, ssl.SSLContext, dict, str]
|
||||||
|
) -> Optional[ssl.SSLContext]:
|
||||||
"""Convert an ssl argument shorthand to an SSLContext object."""
|
"""Convert an ssl argument shorthand to an SSLContext object."""
|
||||||
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
|
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
|
||||||
return ctxdef
|
return ctxdef
|
||||||
|
@ -53,7 +54,9 @@ def shorthand_to_ctx(ctxdef: TlsDef) -> ssl.SSLContext | None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_to_context(ssldef: TlsDefs) -> ssl.SSLContext | None:
|
def process_to_context(
|
||||||
|
ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
|
||||||
|
) -> Optional[ssl.SSLContext]:
|
||||||
"""Process app.run ssl argument from easy formats to full SSLContext."""
|
"""Process app.run ssl argument from easy formats to full SSLContext."""
|
||||||
return (
|
return (
|
||||||
CertSelector(map(shorthand_to_ctx, ssldef))
|
CertSelector(map(shorthand_to_ctx, ssldef))
|
||||||
|
@ -68,9 +71,13 @@ def load_cert_dir(p: str) -> ssl.SSLContext:
|
||||||
keyfile = os.path.join(p, "privkey.pem")
|
keyfile = os.path.join(p, "privkey.pem")
|
||||||
certfile = os.path.join(p, "fullchain.pem")
|
certfile = os.path.join(p, "fullchain.pem")
|
||||||
if not os.access(keyfile, os.R_OK):
|
if not os.access(keyfile, os.R_OK):
|
||||||
raise ValueError(f"Certificate not found or permission denied {keyfile}")
|
raise ValueError(
|
||||||
|
f"Certificate not found or permission denied {keyfile}"
|
||||||
|
)
|
||||||
if not os.access(certfile, os.R_OK):
|
if not os.access(certfile, os.R_OK):
|
||||||
raise ValueError(f"Certificate not found or permission denied {certfile}")
|
raise ValueError(
|
||||||
|
f"Certificate not found or permission denied {certfile}"
|
||||||
|
)
|
||||||
return CertSimple(certfile, keyfile)
|
return CertSimple(certfile, keyfile)
|
||||||
|
|
||||||
|
|
||||||
|
@ -82,7 +89,9 @@ def find_cert(self: CertSelector, server_name: str):
|
||||||
if not server_name:
|
if not server_name:
|
||||||
if self.sanic_fallback:
|
if self.sanic_fallback:
|
||||||
return self.sanic_fallback
|
return self.sanic_fallback
|
||||||
raise ValueError("The client provided no SNI to match for certificate.")
|
raise ValueError(
|
||||||
|
"The client provided no SNI to match for certificate."
|
||||||
|
)
|
||||||
for ctx in self.sanic_select:
|
for ctx in self.sanic_select:
|
||||||
if match_hostname(ctx, server_name):
|
if match_hostname(ctx, server_name):
|
||||||
return ctx
|
return ctx
|
||||||
|
@ -91,7 +100,9 @@ def find_cert(self: CertSelector, server_name: str):
|
||||||
raise ValueError(f"No certificate found matching hostname {server_name!r}")
|
raise ValueError(f"No certificate found matching hostname {server_name!r}")
|
||||||
|
|
||||||
|
|
||||||
def match_hostname(ctx: ssl.SSLContext | CertSelector, hostname: str) -> bool:
|
def match_hostname(
|
||||||
|
ctx: Union[ssl.SSLContext, CertSelector], hostname: str
|
||||||
|
) -> bool:
|
||||||
"""Match names from CertSelector against a received hostname."""
|
"""Match names from CertSelector against a received hostname."""
|
||||||
# Local certs are considered trusted, so this can be less pedantic
|
# Local certs are considered trusted, so this can be less pedantic
|
||||||
# and thus faster than the deprecated ssl.match_hostname function is.
|
# and thus faster than the deprecated ssl.match_hostname function is.
|
||||||
|
@ -108,7 +119,7 @@ def match_hostname(ctx: ssl.SSLContext | CertSelector, hostname: str) -> bool:
|
||||||
|
|
||||||
def selector_sni_callback(
|
def selector_sni_callback(
|
||||||
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
|
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
|
||||||
) -> int | None:
|
) -> Optional[int]:
|
||||||
"""Select a certificate matching the SNI."""
|
"""Select a certificate matching the SNI."""
|
||||||
# Call server_name_callback to store the SNI on sslobj
|
# Call server_name_callback to store the SNI on sslobj
|
||||||
server_name_callback(sslobj, server_name, ctx)
|
server_name_callback(sslobj, server_name, ctx)
|
||||||
|
@ -131,7 +142,7 @@ def server_name_callback(
|
||||||
|
|
||||||
|
|
||||||
class SanicSSLContext(ssl.SSLContext):
|
class SanicSSLContext(ssl.SSLContext):
|
||||||
sanic: dict[str, os.PathLike]
|
sanic: Dict[str, os.PathLike]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_ssl_context(cls, context: ssl.SSLContext):
|
def create_from_ssl_context(cls, context: ssl.SSLContext):
|
||||||
|
@ -142,7 +153,7 @@ class SanicSSLContext(ssl.SSLContext):
|
||||||
class CertSimple(SanicSSLContext):
|
class CertSimple(SanicSSLContext):
|
||||||
"""A wrapper for creating SSLContext with a sanic attribute."""
|
"""A wrapper for creating SSLContext with a sanic attribute."""
|
||||||
|
|
||||||
sanic: dict[str, Any]
|
sanic: Dict[str, Any]
|
||||||
|
|
||||||
def __new__(cls, cert, key, **kw):
|
def __new__(cls, cert, key, **kw):
|
||||||
# try common aliases, rename to cert/key
|
# try common aliases, rename to cert/key
|
||||||
|
@ -155,7 +166,9 @@ class CertSimple(SanicSSLContext):
|
||||||
if "names" not in kw:
|
if "names" not in kw:
|
||||||
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
|
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
|
||||||
kw["names"] = [
|
kw["names"] = [
|
||||||
name for t, name in cert["subjectAltName"] if t in ["DNS", "IP Address"]
|
name
|
||||||
|
for t, name in cert["subjectAltName"]
|
||||||
|
if t in ["DNS", "IP Address"]
|
||||||
]
|
]
|
||||||
subject = {k: v for item in cert["subject"] for k, v in item}
|
subject = {k: v for item in cert["subject"] for k, v in item}
|
||||||
self = create_context(certfile, keyfile, password)
|
self = create_context(certfile, keyfile, password)
|
||||||
|
@ -177,7 +190,7 @@ class CertSelector(ssl.SSLContext):
|
||||||
def __new__(cls, ctxs):
|
def __new__(cls, ctxs):
|
||||||
return super().__new__(cls)
|
return super().__new__(cls)
|
||||||
|
|
||||||
def __init__(self, ctxs: Iterable[ssl.SSLContext | None]):
|
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.sni_callback = selector_sni_callback # type: ignore
|
self.sni_callback = selector_sni_callback # type: ignore
|
||||||
self.sanic_select = []
|
self.sanic_select = []
|
||||||
|
@ -192,5 +205,7 @@ class CertSelector(ssl.SSLContext):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
self.sanic_fallback = ctx
|
self.sanic_fallback = ctx
|
||||||
if not all_names:
|
if not all_names:
|
||||||
raise ValueError("No certificates with SubjectAlternativeNames found.")
|
raise ValueError(
|
||||||
|
"No certificates with SubjectAlternativeNames found."
|
||||||
|
)
|
||||||
logger.info(f"Certificate vhosts: {', '.join(all_names)}")
|
logger.info(f"Certificate vhosts: {', '.join(all_names)}")
|
||||||
|
|
|
@ -3,12 +3,13 @@ from __future__ import annotations
|
||||||
import ssl
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, Optional, Tuple, Type, Union, cast
|
||||||
|
|
||||||
from sanic.application.constants import Mode
|
from sanic.application.constants import Mode
|
||||||
from sanic.application.spinner import loading
|
from sanic.application.spinner import loading
|
||||||
|
@ -21,6 +22,7 @@ from sanic.exceptions import SanicException
|
||||||
from sanic.helpers import Default
|
from sanic.helpers import Default
|
||||||
from sanic.http.tls.context import CertSimple, SanicSSLContext
|
from sanic.http.tls.context import CertSimple, SanicSSLContext
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import trustme
|
import trustme
|
||||||
|
|
||||||
|
@ -45,7 +47,7 @@ CIPHERS_TLS12 = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _make_path(maybe_path: Path | str, tmpdir: Path | None) -> Path:
|
def _make_path(maybe_path: Union[Path, str], tmpdir: Optional[Path]) -> Path:
|
||||||
if isinstance(maybe_path, Path):
|
if isinstance(maybe_path, Path):
|
||||||
return maybe_path
|
return maybe_path
|
||||||
else:
|
else:
|
||||||
|
@ -58,7 +60,9 @@ def _make_path(maybe_path: Path | str, tmpdir: Path | None) -> Path:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def get_ssl_context(app: Sanic, ssl: ssl.SSLContext | None) -> ssl.SSLContext:
|
def get_ssl_context(
|
||||||
|
app: Sanic, ssl: Optional[ssl.SSLContext]
|
||||||
|
) -> ssl.SSLContext:
|
||||||
if ssl:
|
if ssl:
|
||||||
return ssl
|
return ssl
|
||||||
|
|
||||||
|
@ -92,8 +96,16 @@ class CertCreator(ABC):
|
||||||
if isinstance(self.key, Default) or isinstance(self.cert, Default):
|
if isinstance(self.key, Default) or isinstance(self.cert, Default):
|
||||||
self.tmpdir = Path(mkdtemp())
|
self.tmpdir = Path(mkdtemp())
|
||||||
|
|
||||||
key = DEFAULT_LOCAL_TLS_KEY if isinstance(self.key, Default) else self.key
|
key = (
|
||||||
cert = DEFAULT_LOCAL_TLS_CERT if isinstance(self.cert, Default) else self.cert
|
DEFAULT_LOCAL_TLS_KEY
|
||||||
|
if isinstance(self.key, Default)
|
||||||
|
else self.key
|
||||||
|
)
|
||||||
|
cert = (
|
||||||
|
DEFAULT_LOCAL_TLS_CERT
|
||||||
|
if isinstance(self.cert, Default)
|
||||||
|
else self.cert
|
||||||
|
)
|
||||||
|
|
||||||
self.key_path = _make_path(key, self.tmpdir)
|
self.key_path = _make_path(key, self.tmpdir)
|
||||||
self.cert_path = _make_path(cert, self.tmpdir)
|
self.cert_path = _make_path(cert, self.tmpdir)
|
||||||
|
@ -114,9 +126,11 @@ class CertCreator(ABC):
|
||||||
local_tls_key,
|
local_tls_key,
|
||||||
local_tls_cert,
|
local_tls_cert,
|
||||||
) -> CertCreator:
|
) -> CertCreator:
|
||||||
creator: CertCreator | None = None
|
creator: Optional[CertCreator] = None
|
||||||
|
|
||||||
cert_creator_options: tuple[tuple[type[CertCreator], LocalCertCreator], ...] = (
|
cert_creator_options: Tuple[
|
||||||
|
Tuple[Type[CertCreator], LocalCertCreator], ...
|
||||||
|
] = (
|
||||||
(MkcertCreator, LocalCertCreator.MKCERT),
|
(MkcertCreator, LocalCertCreator.MKCERT),
|
||||||
(TrustmeCreator, LocalCertCreator.TRUSTME),
|
(TrustmeCreator, LocalCertCreator.TRUSTME),
|
||||||
)
|
)
|
||||||
|
@ -146,8 +160,8 @@ class CertCreator(ABC):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _try_select(
|
def _try_select(
|
||||||
app: Sanic,
|
app: Sanic,
|
||||||
creator: CertCreator | None,
|
creator: Optional[CertCreator],
|
||||||
creator_class: type[CertCreator],
|
creator_class: Type[CertCreator],
|
||||||
creator_requirement: LocalCertCreator,
|
creator_requirement: LocalCertCreator,
|
||||||
creator_requested: LocalCertCreator,
|
creator_requested: LocalCertCreator,
|
||||||
local_tls_key,
|
local_tls_key,
|
||||||
|
|
18
sanic/log.py
18
sanic/log.py
|
@ -1,11 +1,13 @@
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any, Dict
|
from typing import TYPE_CHECKING, Any, Dict
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from sanic.helpers import is_atty
|
from sanic.helpers import is_atty
|
||||||
|
|
||||||
|
|
||||||
# Python 3.11 changed the way Enum formatting works for mixed-in types.
|
# Python 3.11 changed the way Enum formatting works for mixed-in types.
|
||||||
if sys.version_info < (3, 11, 0):
|
if sys.version_info < (3, 11, 0):
|
||||||
|
|
||||||
|
@ -17,10 +19,10 @@ else:
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
|
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||||
"version": 1,
|
version=1,
|
||||||
"disable_existing_loggers": False,
|
disable_existing_loggers=False,
|
||||||
"loggers": {
|
loggers={
|
||||||
"sanic.root": {"level": "INFO", "handlers": ["console"]},
|
"sanic.root": {"level": "INFO", "handlers": ["console"]},
|
||||||
"sanic.error": {
|
"sanic.error": {
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
|
@ -41,7 +43,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
|
||||||
"qualname": "sanic.server",
|
"qualname": "sanic.server",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"handlers": {
|
handlers={
|
||||||
"console": {
|
"console": {
|
||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "generic",
|
"formatter": "generic",
|
||||||
|
@ -58,7 +60,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
|
||||||
"stream": sys.stdout,
|
"stream": sys.stdout,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"formatters": {
|
formatters={
|
||||||
"generic": {
|
"generic": {
|
||||||
"format": "%(asctime)s [%(process)s] [%(levelname)s] %(message)s",
|
"format": "%(asctime)s [%(process)s] [%(levelname)s] %(message)s",
|
||||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||||
|
@ -66,12 +68,12 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = { # no cov
|
||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
|
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
|
||||||
"%(request)s %(message)s %(status)s %(byte)s",
|
+ "%(request)s %(message)s %(status)s %(byte)s",
|
||||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||||
"class": "logging.Formatter",
|
"class": "logging.Formatter",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
)
|
||||||
"""
|
"""
|
||||||
Defult logging configuration
|
Defult logging configuration
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from enum import IntEnum, auto
|
from enum import IntEnum, auto
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from typing import Sequence
|
from typing import Deque, Sequence, Union
|
||||||
|
|
||||||
from sanic.models.handler_types import MiddlewareType
|
from sanic.models.handler_types import MiddlewareType
|
||||||
|
|
||||||
|
@ -69,9 +69,9 @@ class Middleware:
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert(
|
def convert(
|
||||||
cls,
|
cls,
|
||||||
*middleware_collections: Sequence[Middleware | MiddlewareType],
|
*middleware_collections: Sequence[Union[Middleware, MiddlewareType]],
|
||||||
location: MiddlewareLocation,
|
location: MiddlewareLocation,
|
||||||
) -> deque[Middleware]:
|
) -> Deque[Middleware]:
|
||||||
"""Convert middleware collections to a deque of Middleware objects.
|
"""Convert middleware collections to a deque of Middleware objects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
@ -14,7 +14,7 @@ class ExceptionMixin(metaclass=SanicMeta):
|
||||||
def exception(
|
def exception(
|
||||||
self,
|
self,
|
||||||
*exceptions: Union[Type[Exception], List[Type[Exception]]],
|
*exceptions: Union[Type[Exception], List[Type[Exception]]],
|
||||||
apply: bool = True,
|
apply: bool = True
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
"""Decorator used to register an exception handler for the current application or blueprint instance.
|
"""Decorator used to register an exception handler for the current application or blueprint instance.
|
||||||
|
|
||||||
|
@ -79,7 +79,9 @@ class ExceptionMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def all_exceptions(self, handler: Callable[..., Any]) -> Callable[..., Any]:
|
def all_exceptions(
|
||||||
|
self, handler: Callable[..., Any]
|
||||||
|
) -> Callable[..., Any]:
|
||||||
"""Enables the process of creating a global exception handler as a convenience.
|
"""Enables the process of creating a global exception handler as a convenience.
|
||||||
|
|
||||||
This following two examples are equivalent:
|
This following two examples are equivalent:
|
||||||
|
|
|
@ -120,12 +120,16 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
if callable(listener_or_event):
|
if callable(listener_or_event):
|
||||||
if event_or_none is None:
|
if event_or_none is None:
|
||||||
raise BadRequest("Invalid event registration: Missing event name.")
|
raise BadRequest(
|
||||||
|
"Invalid event registration: Missing event name."
|
||||||
|
)
|
||||||
return register_listener(listener_or_event, event_or_none)
|
return register_listener(listener_or_event, event_or_none)
|
||||||
else:
|
else:
|
||||||
return partial(register_listener, event=listener_or_event)
|
return partial(register_listener, event=listener_or_event)
|
||||||
|
|
||||||
def main_process_start(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def main_process_start(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the main_process_start event.
|
"""Decorator for registering a listener for the main_process_start event.
|
||||||
|
|
||||||
This event is fired only on the main process and **NOT** on any
|
This event is fired only on the main process and **NOT** on any
|
||||||
|
@ -147,7 +151,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
return self.listener(listener, "main_process_start")
|
return self.listener(listener, "main_process_start")
|
||||||
|
|
||||||
def main_process_ready(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def main_process_ready(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the main_process_ready event.
|
"""Decorator for registering a listener for the main_process_ready event.
|
||||||
|
|
||||||
This event is fired only on the main process and **NOT** on any
|
This event is fired only on the main process and **NOT** on any
|
||||||
|
@ -170,7 +176,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
return self.listener(listener, "main_process_ready")
|
return self.listener(listener, "main_process_ready")
|
||||||
|
|
||||||
def main_process_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def main_process_stop(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the main_process_stop event.
|
"""Decorator for registering a listener for the main_process_stop event.
|
||||||
|
|
||||||
This event is fired only on the main process and **NOT** on any
|
This event is fired only on the main process and **NOT** on any
|
||||||
|
@ -214,7 +222,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
return self.listener(listener, "reload_process_start")
|
return self.listener(listener, "reload_process_start")
|
||||||
|
|
||||||
def reload_process_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def reload_process_stop(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the reload_process_stop event.
|
"""Decorator for registering a listener for the reload_process_stop event.
|
||||||
|
|
||||||
This event is fired only on the reload process and **NOT** on any
|
This event is fired only on the reload process and **NOT** on any
|
||||||
|
@ -283,7 +293,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
return self.listener(listener, "after_reload_trigger")
|
return self.listener(listener, "after_reload_trigger")
|
||||||
|
|
||||||
def before_server_start(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def before_server_start(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the before_server_start event.
|
"""Decorator for registering a listener for the before_server_start event.
|
||||||
|
|
||||||
This event is fired on all worker processes. You should typically
|
This event is fired on all worker processes. You should typically
|
||||||
|
@ -307,7 +319,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
return self.listener(listener, "before_server_start")
|
return self.listener(listener, "before_server_start")
|
||||||
|
|
||||||
def after_server_start(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def after_server_start(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the after_server_start event.
|
"""Decorator for registering a listener for the after_server_start event.
|
||||||
|
|
||||||
This event is fired on all worker processes. You should typically
|
This event is fired on all worker processes. You should typically
|
||||||
|
@ -335,7 +349,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
return self.listener(listener, "after_server_start")
|
return self.listener(listener, "after_server_start")
|
||||||
|
|
||||||
def before_server_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def before_server_stop(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the before_server_stop event.
|
"""Decorator for registering a listener for the before_server_stop event.
|
||||||
|
|
||||||
This event is fired on all worker processes. This event is fired
|
This event is fired on all worker processes. This event is fired
|
||||||
|
@ -360,7 +376,9 @@ class ListenerMixin(metaclass=SanicMeta):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
return self.listener(listener, "before_server_stop")
|
return self.listener(listener, "before_server_stop")
|
||||||
|
|
||||||
def after_server_stop(self, listener: ListenerType[Sanic]) -> ListenerType[Sanic]:
|
def after_server_stop(
|
||||||
|
self, listener: ListenerType[Sanic]
|
||||||
|
) -> ListenerType[Sanic]:
|
||||||
"""Decorator for registering a listener for the after_server_stop event.
|
"""Decorator for registering a listener for the after_server_stop event.
|
||||||
|
|
||||||
This event is fired on all worker processes. This event is fired
|
This event is fired on all worker processes. This event is fired
|
||||||
|
|
|
@ -25,7 +25,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||||
attach_to: str = "request",
|
attach_to: str = "request",
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
*,
|
*,
|
||||||
priority: int = 0,
|
priority: int = 0
|
||||||
) -> MiddlewareType:
|
) -> MiddlewareType:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||||
attach_to: str = "request",
|
attach_to: str = "request",
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
*,
|
*,
|
||||||
priority: int = 0,
|
priority: int = 0
|
||||||
) -> Callable[[MiddlewareType], MiddlewareType]:
|
) -> Callable[[MiddlewareType], MiddlewareType]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||||
attach_to: str = "request",
|
attach_to: str = "request",
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
*,
|
*,
|
||||||
priority: int = 0,
|
priority: int = 0
|
||||||
) -> Union[MiddlewareType, Callable[[MiddlewareType], MiddlewareType]]:
|
) -> Union[MiddlewareType, Callable[[MiddlewareType], MiddlewareType]]:
|
||||||
"""Decorator for registering middleware.
|
"""Decorator for registering middleware.
|
||||||
|
|
||||||
|
@ -99,9 +99,13 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
# Detect which way this was called, @middleware or @middleware('AT')
|
# Detect which way this was called, @middleware or @middleware('AT')
|
||||||
if callable(middleware_or_request):
|
if callable(middleware_or_request):
|
||||||
return register_middleware(middleware_or_request, attach_to=attach_to)
|
return register_middleware(
|
||||||
|
middleware_or_request, attach_to=attach_to
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return partial(register_middleware, attach_to=middleware_or_request)
|
return partial(
|
||||||
|
register_middleware, attach_to=middleware_or_request
|
||||||
|
)
|
||||||
|
|
||||||
def on_request(self, middleware=None, *, priority=0) -> MiddlewareType:
|
def on_request(self, middleware=None, *, priority=0) -> MiddlewareType:
|
||||||
"""Register a middleware to be called before a request is handled.
|
"""Register a middleware to be called before a request is handled.
|
||||||
|
@ -153,7 +157,9 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||||
if callable(middleware):
|
if callable(middleware):
|
||||||
return self.middleware(middleware, "response", priority=priority)
|
return self.middleware(middleware, "response", priority=priority)
|
||||||
else:
|
else:
|
||||||
return partial(self.middleware, attach_to="response", priority=priority)
|
return partial(
|
||||||
|
self.middleware, attach_to="response", priority=priority
|
||||||
|
)
|
||||||
|
|
||||||
def finalize_middleware(self) -> None:
|
def finalize_middleware(self) -> None:
|
||||||
"""Finalize the middleware configuration for the Sanic application.
|
"""Finalize the middleware configuration for the Sanic application.
|
||||||
|
|
|
@ -25,7 +25,10 @@ from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.types import HashableDict
|
from sanic.types import HashableDict
|
||||||
|
|
||||||
RouteWrapper = Callable[[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]]
|
|
||||||
|
RouteWrapper = Callable[
|
||||||
|
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||||
|
@ -812,5 +815,7 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta):
|
||||||
}
|
}
|
||||||
if raw:
|
if raw:
|
||||||
unexpected_arguments = ", ".join(raw.keys())
|
unexpected_arguments = ", ".join(raw.keys())
|
||||||
raise TypeError(f"Unexpected keyword arguments: {unexpected_arguments}")
|
raise TypeError(
|
||||||
|
f"Unexpected keyword arguments: {unexpected_arguments}"
|
||||||
|
)
|
||||||
return HashableDict(ctx_kwargs)
|
return HashableDict(ctx_kwargs)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable, Coroutine
|
from typing import Any, Callable, Coroutine, Dict, Optional, Set, Union
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
from sanic.models.futures import FutureSignal
|
from sanic.models.futures import FutureSignal
|
||||||
|
@ -12,17 +12,17 @@ from sanic.types import HashableDict
|
||||||
|
|
||||||
class SignalMixin(metaclass=SanicMeta):
|
class SignalMixin(metaclass=SanicMeta):
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_signals: set[FutureSignal] = set()
|
self._future_signals: Set[FutureSignal] = set()
|
||||||
|
|
||||||
def _apply_signal(self, signal: FutureSignal) -> Signal:
|
def _apply_signal(self, signal: FutureSignal) -> Signal:
|
||||||
raise NotImplementedError # noqa
|
raise NotImplementedError # noqa
|
||||||
|
|
||||||
def signal(
|
def signal(
|
||||||
self,
|
self,
|
||||||
event: str | Enum,
|
event: Union[str, Enum],
|
||||||
*,
|
*,
|
||||||
apply: bool = True,
|
apply: bool = True,
|
||||||
condition: dict[str, Any] | None = None,
|
condition: Optional[Dict[str, Any]] = None,
|
||||||
exclusive: bool = True,
|
exclusive: bool = True,
|
||||||
) -> Callable[[SignalHandler], SignalHandler]:
|
) -> Callable[[SignalHandler], SignalHandler]:
|
||||||
"""
|
"""
|
||||||
|
@ -64,9 +64,9 @@ class SignalMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
def add_signal(
|
def add_signal(
|
||||||
self,
|
self,
|
||||||
handler: Callable[..., Any] | None,
|
handler: Optional[Callable[..., Any]],
|
||||||
event: str,
|
event: str,
|
||||||
condition: dict[str, Any] | None = None,
|
condition: Optional[Dict[str, Any]] = None,
|
||||||
exclusive: bool = True,
|
exclusive: bool = True,
|
||||||
) -> Callable[..., Any]:
|
) -> Callable[..., Any]:
|
||||||
"""Registers a signal handler for a specific event.
|
"""Registers a signal handler for a specific event.
|
||||||
|
@ -92,7 +92,9 @@ class SignalMixin(metaclass=SanicMeta):
|
||||||
...
|
...
|
||||||
|
|
||||||
handler = noop
|
handler = noop
|
||||||
self.signal(event=event, condition=condition, exclusive=exclusive)(handler)
|
self.signal(event=event, condition=condition, exclusive=exclusive)(
|
||||||
|
handler
|
||||||
|
)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def event(self, event: str):
|
def event(self, event: str):
|
||||||
|
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import sys
|
||||||
|
|
||||||
from asyncio import (
|
from asyncio import (
|
||||||
AbstractEventLoop,
|
AbstractEventLoop,
|
||||||
CancelledError,
|
CancelledError,
|
||||||
|
@ -30,7 +32,13 @@ from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
@ -63,6 +71,7 @@ from sanic.worker.multiplexer import WorkerMultiplexer
|
||||||
from sanic.worker.reloader import Reloader
|
from sanic.worker.reloader import Reloader
|
||||||
from sanic.worker.serve import worker_serve
|
from sanic.worker.serve import worker_serve
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.application.state import ApplicationState
|
from sanic.application.state import ApplicationState
|
||||||
|
@ -70,17 +79,20 @@ 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): # no cov
|
||||||
|
HTTPVersion = Union[HTTP, int]
|
||||||
|
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 StartupMixin(metaclass=SanicMeta):
|
class StartupMixin(metaclass=SanicMeta):
|
||||||
_app_registry: ClassVar[dict[str, Sanic]]
|
_app_registry: ClassVar[Dict[str, Sanic]]
|
||||||
|
|
||||||
asgi: bool
|
asgi: bool
|
||||||
config: Config
|
config: Config
|
||||||
listeners: dict[str, list[ListenerType[Any]]]
|
listeners: Dict[str, List[ListenerType[Any]]]
|
||||||
state: ApplicationState
|
state: ApplicationState
|
||||||
websocket_enabled: bool
|
websocket_enabled: bool
|
||||||
multiplexer: WorkerMultiplexer
|
multiplexer: WorkerMultiplexer
|
||||||
|
@ -100,7 +112,8 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
"""
|
"""
|
||||||
if not self.asgi:
|
if not self.asgi:
|
||||||
if self.config.USE_UVLOOP is True or (
|
if self.config.USE_UVLOOP is True or (
|
||||||
isinstance(self.config.USE_UVLOOP, Default) and not OS_IS_WINDOWS
|
isinstance(self.config.USE_UVLOOP, Default)
|
||||||
|
and not OS_IS_WINDOWS
|
||||||
):
|
):
|
||||||
try_use_uvloop()
|
try_use_uvloop()
|
||||||
elif OS_IS_WINDOWS:
|
elif OS_IS_WINDOWS:
|
||||||
|
@ -146,28 +159,28 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host: str | None = None,
|
host: Optional[str] = None,
|
||||||
port: int | None = None,
|
port: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
dev: bool = False,
|
dev: bool = False,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
auto_reload: bool | None = None,
|
auto_reload: Optional[bool] = None,
|
||||||
version: HTTPVersion = HTTP.VERSION_1,
|
version: HTTPVersion = HTTP.VERSION_1,
|
||||||
ssl: None | SSLContext | dict | str | list | tuple = None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock: socket | None = None,
|
sock: Optional[socket] = None,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
protocol: type[Protocol] | None = None,
|
protocol: Optional[Type[Protocol]] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
register_sys_signals: bool = True,
|
register_sys_signals: bool = True,
|
||||||
access_log: bool | None = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: str | None = None,
|
unix: Optional[str] = None,
|
||||||
loop: AbstractEventLoop | None = None,
|
loop: Optional[AbstractEventLoop] = None,
|
||||||
reload_dir: list[str] | str | None = None,
|
reload_dir: Optional[Union[List[str], str]] = None,
|
||||||
noisy_exceptions: bool | None = 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: dict[str, str] | None = None,
|
motd_display: Optional[Dict[str, str]] = None,
|
||||||
auto_tls: bool = False,
|
auto_tls: bool = False,
|
||||||
single_process: bool = False,
|
single_process: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -276,28 +289,28 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
def prepare(
|
def prepare(
|
||||||
self,
|
self,
|
||||||
host: str | None = None,
|
host: Optional[str] = None,
|
||||||
port: int | None = None,
|
port: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
dev: bool = False,
|
dev: bool = False,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
auto_reload: bool | None = None,
|
auto_reload: Optional[bool] = None,
|
||||||
version: HTTPVersion = HTTP.VERSION_1,
|
version: HTTPVersion = HTTP.VERSION_1,
|
||||||
ssl: None | SSLContext | dict | str | list | tuple = None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock: socket | None = None,
|
sock: Optional[socket] = None,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
protocol: type[Protocol] | None = None,
|
protocol: Optional[Type[Protocol]] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
register_sys_signals: bool = True,
|
register_sys_signals: bool = True,
|
||||||
access_log: bool | None = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: str | None = None,
|
unix: Optional[str] = None,
|
||||||
loop: AbstractEventLoop | None = None,
|
loop: Optional[AbstractEventLoop] = None,
|
||||||
reload_dir: list[str] | str | None = None,
|
reload_dir: Optional[Union[List[str], str]] = None,
|
||||||
noisy_exceptions: bool | None = 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: dict[str, str] | None = None,
|
motd_display: Optional[Dict[str, str]] = None,
|
||||||
coffee: bool = False,
|
coffee: bool = False,
|
||||||
auto_tls: bool = False,
|
auto_tls: bool = False,
|
||||||
single_process: bool = False,
|
single_process: bool = False,
|
||||||
|
@ -372,7 +385,8 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
if single_process and (fast or (workers > 1) or auto_reload):
|
if single_process and (fast or (workers > 1) or auto_reload):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Single process cannot be run with multiple workers " "or auto-reload"
|
"Single process cannot be run with multiple workers "
|
||||||
|
"or auto-reload"
|
||||||
)
|
)
|
||||||
|
|
||||||
if register_sys_signals is False and not single_process:
|
if register_sys_signals is False and not single_process:
|
||||||
|
@ -391,7 +405,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
for directory in reload_dir:
|
for directory in reload_dir:
|
||||||
direc = Path(directory)
|
direc = Path(directory)
|
||||||
if not direc.is_dir():
|
if not direc.is_dir():
|
||||||
logger.warning(f"Directory {directory} could not be located")
|
logger.warning(
|
||||||
|
f"Directory {directory} could not be located"
|
||||||
|
)
|
||||||
self.state.reload_dirs.add(Path(directory))
|
self.state.reload_dirs.add(Path(directory))
|
||||||
|
|
||||||
if loop is not None:
|
if loop is not None:
|
||||||
|
@ -406,7 +422,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
host, port = self.get_address(host, port, version, auto_tls)
|
host, port = self.get_address(host, port, version, auto_tls)
|
||||||
|
|
||||||
if protocol is None:
|
if protocol is None:
|
||||||
protocol = WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
protocol = (
|
||||||
|
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||||
|
)
|
||||||
|
|
||||||
# Set explicitly passed configuration values
|
# Set explicitly passed configuration values
|
||||||
for attribute, value in {
|
for attribute, value in {
|
||||||
|
@ -442,7 +460,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
register_sys_signals=register_sys_signals,
|
register_sys_signals=register_sys_signals,
|
||||||
auto_tls=auto_tls,
|
auto_tls=auto_tls,
|
||||||
)
|
)
|
||||||
self.state.server_info.append(ApplicationServerInfo(settings=server_settings))
|
self.state.server_info.append(
|
||||||
|
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
|
||||||
|
@ -451,20 +471,20 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
async def create_server(
|
async def create_server(
|
||||||
self,
|
self,
|
||||||
host: str | None = None,
|
host: Optional[str] = None,
|
||||||
port: int | None = None,
|
port: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
ssl: None | SSLContext | dict | str | list | tuple = None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock: socket | None = None,
|
sock: Optional[socket] = None,
|
||||||
protocol: type[Protocol] | None = None,
|
protocol: Optional[Type[Protocol]] = None,
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
access_log: bool | None = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: str | None = None,
|
unix: Optional[str] = None,
|
||||||
return_asyncio_server: bool = True,
|
return_asyncio_server: bool = True,
|
||||||
asyncio_server_kwargs: dict[str, Any] | None = None,
|
asyncio_server_kwargs: Optional[Dict[str, Any]] = None,
|
||||||
noisy_exceptions: bool | None = None,
|
noisy_exceptions: Optional[bool] = None,
|
||||||
) -> AsyncioServer | None:
|
) -> Optional[AsyncioServer]:
|
||||||
"""
|
"""
|
||||||
Low level API for creating a Sanic Server instance.
|
Low level API for creating a Sanic Server instance.
|
||||||
|
|
||||||
|
@ -538,7 +558,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
host, port = host, port = self.get_address(host, port)
|
host, port = host, port = self.get_address(host, port)
|
||||||
|
|
||||||
if protocol is None:
|
if protocol is None:
|
||||||
protocol = WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
protocol = (
|
||||||
|
WebSocketProtocol if self.websocket_enabled else HttpProtocol
|
||||||
|
)
|
||||||
|
|
||||||
# Set explicitly passed configuration values
|
# Set explicitly passed configuration values
|
||||||
for attribute, value in {
|
for attribute, value in {
|
||||||
|
@ -615,21 +637,21 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
def _helper(
|
def _helper(
|
||||||
self,
|
self,
|
||||||
host: str | None = None,
|
host: Optional[str] = None,
|
||||||
port: int | None = None,
|
port: Optional[int] = None,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
version: HTTPVersion = HTTP.VERSION_1,
|
version: HTTPVersion = HTTP.VERSION_1,
|
||||||
ssl: None | SSLContext | dict | str | list | tuple = None,
|
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
|
||||||
sock: socket | None = None,
|
sock: Optional[socket] = None,
|
||||||
unix: str | None = None,
|
unix: Optional[str] = None,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
loop: AbstractEventLoop | None = 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,
|
||||||
run_async: bool = False,
|
run_async: bool = False,
|
||||||
auto_tls: bool = False,
|
auto_tls: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Helper function used by `run` and `create_server`."""
|
"""Helper function used by `run` and `create_server`."""
|
||||||
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -704,7 +726,7 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
def motd(
|
def motd(
|
||||||
self,
|
self,
|
||||||
server_settings: dict[str, Any] | None = None,
|
server_settings: Optional[Dict[str, Any]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Outputs the message of the day (MOTD).
|
"""Outputs the message of the day (MOTD).
|
||||||
|
|
||||||
|
@ -733,8 +755,8 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
MOTD.output(logo, serve_location, display, extra)
|
MOTD.output(logo, serve_location, display, extra)
|
||||||
|
|
||||||
def get_motd_data(
|
def get_motd_data(
|
||||||
self, server_settings: dict[str, Any] | None = None
|
self, server_settings: Optional[Dict[str, Any]] = None
|
||||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
||||||
"""Retrieves the message of the day (MOTD) data.
|
"""Retrieves the message of the day (MOTD) data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -780,7 +802,10 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
reload_display += ", ".join(
|
reload_display += ", ".join(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
*(str(path.absolute()) for path in self.state.reload_dirs),
|
*(
|
||||||
|
str(path.absolute())
|
||||||
|
for path in self.state.reload_dirs
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
display["auto-reload"] = reload_display
|
display["auto-reload"] = reload_display
|
||||||
|
@ -819,7 +844,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
return f"http://<{location}>"
|
return f"http://<{location}>"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_server_location(server_settings: dict[str, Any] | None = None) -> str:
|
def get_server_location(
|
||||||
|
server_settings: Optional[Dict[str, Any]] = None
|
||||||
|
) -> str:
|
||||||
"""Using the server settings, retrieve the server location.
|
"""Using the server settings, retrieve the server location.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -853,11 +880,11 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_address(
|
def get_address(
|
||||||
host: str | None,
|
host: Optional[str],
|
||||||
port: int | None,
|
port: Optional[int],
|
||||||
version: HTTPVersion = HTTP.VERSION_1,
|
version: HTTPVersion = HTTP.VERSION_1,
|
||||||
auto_tls: bool = False,
|
auto_tls: bool = False,
|
||||||
) -> tuple[str, int]:
|
) -> Tuple[str, int]:
|
||||||
"""Retrieve the host address and port, with default values based on the given parameters.
|
"""Retrieve the host address and port, with default values based on the given parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -886,7 +913,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_startup_method(cls) -> str:
|
def _get_startup_method(cls) -> str:
|
||||||
return (
|
return (
|
||||||
cls.start_method if not isinstance(cls.start_method, Default) else "spawn"
|
cls.start_method
|
||||||
|
if not isinstance(cls.start_method, Default)
|
||||||
|
else "spawn"
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -913,10 +942,10 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
@classmethod
|
@classmethod
|
||||||
def serve(
|
def serve(
|
||||||
cls,
|
cls,
|
||||||
primary: Sanic | None = None,
|
primary: Optional[Sanic] = None,
|
||||||
*,
|
*,
|
||||||
app_loader: AppLoader | None = None,
|
app_loader: Optional[AppLoader] = None,
|
||||||
factory: Callable[[], Sanic] | None = None,
|
factory: Optional[Callable[[], Sanic]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Serve one or more Sanic applications.
|
"""Serve one or more Sanic applications.
|
||||||
|
|
||||||
|
@ -967,7 +996,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
try:
|
try:
|
||||||
primary = apps[0]
|
primary = apps[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise RuntimeError("Did not find any applications.") from None
|
raise RuntimeError(
|
||||||
|
"Did not find any applications."
|
||||||
|
) from None
|
||||||
|
|
||||||
# This exists primarily for unit testing
|
# This exists primarily for unit testing
|
||||||
if not primary.state.server_info: # no cov
|
if not primary.state.server_info: # no cov
|
||||||
|
@ -1009,7 +1040,7 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
primary_server_info.settings["run_multiple"] = True
|
primary_server_info.settings["run_multiple"] = True
|
||||||
monitor_sub, monitor_pub = Pipe(True)
|
monitor_sub, monitor_pub = Pipe(True)
|
||||||
worker_state: Mapping[str, Any] = sync_manager.dict()
|
worker_state: Mapping[str, Any] = sync_manager.dict()
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: Dict[str, Any] = {
|
||||||
**primary_server_info.settings,
|
**primary_server_info.settings,
|
||||||
"monitor_publisher": monitor_pub,
|
"monitor_publisher": monitor_pub,
|
||||||
"worker_state": worker_state,
|
"worker_state": worker_state,
|
||||||
|
@ -1061,7 +1092,7 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
worker_state,
|
worker_state,
|
||||||
)
|
)
|
||||||
if cls.should_auto_reload():
|
if cls.should_auto_reload():
|
||||||
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 = Reloader(monitor_pub, 0, reload_dirs, app_loader)
|
reloader = Reloader(monitor_pub, 0, reload_dirs, app_loader)
|
||||||
|
@ -1070,7 +1101,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
inspector = None
|
inspector = None
|
||||||
if primary.config.INSPECTOR:
|
if primary.config.INSPECTOR:
|
||||||
display, extra = primary.get_motd_data()
|
display, extra = primary.get_motd_data()
|
||||||
packages = [pkg.strip() for pkg in display["packages"].split(",")]
|
packages = [
|
||||||
|
pkg.strip() for pkg in display["packages"].split(",")
|
||||||
|
]
|
||||||
module = import_module("sanic")
|
module = import_module("sanic")
|
||||||
sanic_version = f"sanic=={module.__version__}" # type: ignore
|
sanic_version = f"sanic=={module.__version__}" # type: ignore
|
||||||
app_info = {
|
app_info = {
|
||||||
|
@ -1101,7 +1134,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
except BaseException:
|
except BaseException:
|
||||||
kwargs = primary_server_info.settings
|
kwargs = primary_server_info.settings
|
||||||
error_logger.exception("Experienced exception while trying to serve")
|
error_logger.exception(
|
||||||
|
"Experienced exception while trying to serve"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
logger.info("Server Stopped")
|
logger.info("Server Stopped")
|
||||||
|
@ -1129,7 +1164,7 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
os._exit(exit_code)
|
os._exit(exit_code)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serve_single(cls, primary: Sanic | None = None) -> None:
|
def serve_single(cls, primary: Optional[Sanic] = None) -> None:
|
||||||
"""Serve a single process of a Sanic application.
|
"""Serve a single process of a Sanic application.
|
||||||
|
|
||||||
Similar to `serve`, but only serves a single process. When used,
|
Similar to `serve`, but only serves a single process. When used,
|
||||||
|
@ -1207,7 +1242,9 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
try:
|
try:
|
||||||
worker_serve(monitor_publisher=None, **kwargs)
|
worker_serve(monitor_publisher=None, **kwargs)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
error_logger.exception("Experienced exception while trying to serve")
|
error_logger.exception(
|
||||||
|
"Experienced exception while trying to serve"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
logger.info("Server Stopped")
|
logger.info("Server Stopped")
|
||||||
|
@ -1226,7 +1263,7 @@ class StartupMixin(metaclass=SanicMeta):
|
||||||
self,
|
self,
|
||||||
primary: Sanic,
|
primary: Sanic,
|
||||||
_,
|
_,
|
||||||
apps: list[Sanic],
|
apps: List[Sanic],
|
||||||
) -> None:
|
) -> None:
|
||||||
for app in apps:
|
for app in apps:
|
||||||
if (
|
if (
|
||||||
|
@ -1271,7 +1308,7 @@ class StartupMixin(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] = {
|
serve_args: Dict[str, Any] = {
|
||||||
**server_info.settings,
|
**server_info.settings,
|
||||||
"run_async": True,
|
"run_async": True,
|
||||||
"reuse_port": bool(primary.state.workers - 1),
|
"reuse_port": bool(primary.state.workers - 1),
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user