sanic/sanic/app.py

1439 lines
50 KiB
Python
Raw Normal View History

import logging
import logging.config
2018-10-18 05:20:16 +01:00
import os
2017-02-09 01:37:32 +00:00
import re
import warnings
2018-10-18 05:20:16 +01:00
2019-01-02 00:35:25 +00:00
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
2018-10-18 05:20:16 +01:00
from collections import defaultdict, deque
from functools import partial
2018-01-22 06:12:41 +00:00
from inspect import getmodulename, isawaitable, signature, stack
from socket import socket
2019-01-02 00:35:25 +00:00
from ssl import Purpose, SSLContext, create_default_context
2016-10-15 20:59:00 +01:00
from traceback import format_exc
from typing import Any, Dict, Optional, Type, Union
2017-02-02 17:21:14 +00:00
from urllib.parse import urlencode, urlunparse
2018-10-18 05:20:16 +01:00
2018-10-23 22:53:39 +01:00
from sanic import reloader_helpers
from sanic.asgi import ASGIApp
Enable Middleware Support for Blueprint Groups (#1399) * enable blueprint group middleware support This commit will enable the users to implement a middleware at the blueprint group level whereby enforcing the middleware automatically to each of the available Blueprints that are part of the group. This will eanble a simple way in which a certain set of common features and criteria can be enforced on a Blueprint group. i.e. authentication and authorization This commit will address the feature request raised as part of Issue #1386 Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * enable indexing of BlueprintGroup object Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * rename blueprint group file to fix spelling error Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add documentation and additional unit tests Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup and optimize headers in unit test file Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix Bluprint Group iteratable method Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add additional unit test to check StopIteration condition Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup iter protocol implemenation for blueprint group and add slots Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix blueprint group middleware invocation identification Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * feat: enable list behavior on blueprint group object and use append instead of properly to add blueprint to group Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-03 22:26:05 +00:00
from sanic.blueprint_group import BlueprintGroup
2018-12-30 11:18:06 +00:00
from sanic.config import BASE_LOGO, Config
2017-02-16 02:54:00 +00:00
from sanic.constants import HTTP_METHODS
2018-10-18 05:20:16 +01:00
from sanic.exceptions import SanicException, ServerError, URLBuildError
2017-02-16 02:54:00 +00:00
from sanic.handlers import ErrorHandler
2018-10-18 05:20:16 +01:00
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
2017-02-21 16:28:45 +00:00
from sanic.response import HTTPResponse, StreamingHTTPResponse
2017-02-16 02:54:00 +00:00
from sanic.router import Router
from sanic.server import (
AsyncioServer,
HttpProtocol,
Signal,
serve,
serve_multiple,
)
2017-02-16 02:54:00 +00:00
from sanic.static import register as static_register
2019-05-21 23:42:19 +01:00
from sanic.testing import SanicASGITestClient, SanicTestClient
2017-02-16 02:54:00 +00:00
from sanic.views import CompositionView
2018-10-18 05:20:16 +01:00
from sanic.websocket import ConnectionClosed, WebSocketProtocol
2016-10-15 20:59:00 +01:00
class Sanic:
2018-10-14 01:55:33 +01:00
def __init__(
self,
name=None,
router=None,
error_handler=None,
load_env=True,
request_class=None,
strict_slashes=False,
log_config=None,
configure_logging=True,
):
# Get name from previous stack frame
2016-11-19 01:06:16 +00:00
if name is None:
warnings.warn(
"Sanic(name=None) is deprecated and None value support "
"for `name` will be removed in the next release. "
"Please use Sanic(name='your_application_name') instead.",
DeprecationWarning,
stacklevel=2,
)
2016-11-19 01:06:16 +00:00
frame_records = stack()[1]
name = getmodulename(frame_records[1])
2017-09-13 07:42:42 +01:00
# logging
if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
2017-09-13 07:42:42 +01:00
2016-10-15 20:59:00 +01:00
self.name = name
2019-06-04 08:58:00 +01:00
self.asgi = False
2016-10-15 20:59:00 +01:00
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
self._blueprint_order = []
self.configure_logging = configure_logging
self.debug = None
2017-01-07 23:46:43 +00:00
self.sock = None
self.strict_slashes = strict_slashes
self.listeners = defaultdict(list)
Fix Ctrl+C and tests on Windows. (#1808) * Fix Ctrl+C on Windows. * Disable testing of a function N/A on Windows. * Add test for coverage, avoid crash on missing _stopping. * Initialise StreamingHTTPResponse.protocol = None * Improved comments. * Reduce amount of data in test_request_stream to avoid failures on Windows. * The Windows test doesn't work on Windows :( * Use port numbers more likely to be free than 8000. * Disable the other signal tests on Windows as well. * Windows doesn't properly support SO_REUSEADDR, so that's disabled in Python, and thus rebinding fails. For successful testing, reuse port instead. * app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests * Revert "app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests" This reverts commit dc5d682448e3f6595bdca5cb764e5f26ca29e295. * Use random test server port on most tests. Should avoid port/addr reuse issues. * Another test to random port instead of 8000. * Fix deprecation warnings about missing name on Sanic() in tests. * Linter and typing * Increase test coverage * Rewrite test for ctrlc_windows_workaround * py36 compat * py36 compat * py36 compat * Don't rely on loop internals but add a stopping flag to app. * App may be restarted. * py36 compat * Linter * Add a constant for OS checking. Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-26 04:42:46 +00:00
self.is_stopping = False
self.is_running = False
2017-05-05 12:09:32 +01:00
self.is_request_stream = False
2017-02-20 22:32:14 +00:00
self.websocket_enabled = False
2017-10-11 12:02:26 +01:00
self.websocket_tasks = set()
self.named_request_middleware = {}
self.named_response_middleware = {}
# Register alternative method names
self.go_fast = self.run
2017-02-12 00:27:38 +00:00
@property
def loop(self):
"""Synonymous with asyncio.get_event_loop().
Only supported when using the `app.run` method.
"""
2019-05-26 22:57:50 +01:00
if not self.is_running and self.asgi is False:
raise SanicException(
2018-10-14 01:55:33 +01:00
"Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function"
)
2017-02-12 00:27:38 +00:00
return get_event_loop()
2016-10-15 20:59:00 +01:00
# -------------------------------------------------------------------- #
# Registration
# -------------------------------------------------------------------- #
2017-02-12 20:07:59 +00:00
def add_task(self, task):
"""Schedule a task to run later, after the loop has started.
2017-02-11 22:52:40 +00:00
Different from asyncio.ensure_future in that it does not
also return a future, and the actual ensure_future call
is delayed until before server start.
:param task: future, couroutine or awaitable
2017-02-11 22:52:40 +00:00
"""
2017-12-22 07:27:34 +00:00
try:
2017-02-11 22:52:40 +00:00
if callable(task):
try:
self.loop.create_task(task(self))
except TypeError:
self.loop.create_task(task())
2017-02-11 22:52:40 +00:00
else:
2017-12-22 07:27:34 +00:00
self.loop.create_task(task)
except SanicException:
2018-10-14 01:55:33 +01:00
@self.listener("before_server_start")
2017-12-22 07:27:34 +00:00
def run(app, loop):
if callable(task):
try:
loop.create_task(task(self))
except TypeError:
loop.create_task(task())
2017-12-22 07:27:34 +00:00
else:
loop.create_task(task)
2017-02-11 22:52:40 +00:00
# Decorator
def listener(self, event):
"""Create a listener from a decorated function.
:param event: event to listen to
"""
2018-01-22 06:12:41 +00:00
def decorator(listener):
self.listeners[event].append(listener)
return listener
2018-01-22 06:12:41 +00:00
return decorator
2017-02-11 22:52:40 +00:00
def register_listener(self, listener, event):
"""
Register the listener for a given event.
:param listener: callable i.e. setup_db(app, loop)
:param event: when to register listener i.e. 'before_server_start'
:return: listener
"""
return self.listener(event)(listener)
2016-10-15 20:59:00 +01:00
# Decorator
2018-10-14 01:55:33 +01:00
def route(
self,
uri,
methods=frozenset({"GET"}),
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""Decorate a function to be registered as a route
2016-10-15 20:59:00 +01:00
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:param host:
2017-05-05 12:09:32 +01:00
:param strict_slashes:
:param stream:
2017-08-21 11:05:34 +01:00
:param version:
:param name: user defined route name for url_for
:return: tuple of routes, decorated function
2016-10-15 20:59:00 +01:00
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
2018-10-14 01:55:33 +01:00
if not uri.startswith("/"):
uri = "/" + uri
2017-05-05 12:09:32 +01:00
if stream:
self.is_request_stream = True
if strict_slashes is None:
strict_slashes = self.strict_slashes
2016-10-15 20:59:00 +01:00
def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
2019-04-06 20:26:49 +01:00
args = list(signature(handler).parameters.keys())
if not args:
handler_name = handler.__name__
2018-01-22 06:52:30 +00:00
raise ValueError(
f"Required parameter `request` missing "
f"in the {handler_name}() route?"
2018-10-14 01:55:33 +01:00
)
2016-10-15 20:59:00 +01:00
2019-04-06 20:26:49 +01:00
if stream:
handler.is_stream = stream
routes.extend(
self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
2019-04-06 20:26:49 +01:00
)
return routes, handler
2019-04-06 20:26:49 +01:00
2016-10-15 20:59:00 +01:00
return response
2017-01-20 07:47:07 +00:00
# Shorthand method decorators
2018-10-14 01:55:33 +01:00
def get(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **GET** *HTTP* method
:param uri: URL to be tagged to **GET** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
2018-10-14 01:55:33 +01:00
return self.route(
uri,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def post(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""
Add an API URL under the **POST** *HTTP* method
:param uri: URL to be tagged to **POST** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
2018-10-14 01:55:33 +01:00
return self.route(
uri,
methods=frozenset({"POST"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def put(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""
Add an API URL under the **PUT** *HTTP* method
:param uri: URL to be tagged to **PUT** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
2018-10-14 01:55:33 +01:00
return self.route(
uri,
methods=frozenset({"PUT"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def head(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
return self.route(
uri,
methods=frozenset({"HEAD"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def options(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **OPTIONS** *HTTP* method
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
2018-10-14 01:55:33 +01:00
return self.route(
uri,
methods=frozenset({"OPTIONS"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def patch(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""
2019-04-06 20:23:50 +01:00
Add an API URL under the **PATCH** *HTTP* method
:param uri: URL to be tagged to **PATCH** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
2018-10-14 01:55:33 +01:00
return self.route(
uri,
methods=frozenset({"PATCH"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def delete(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **DELETE** *HTTP* method
:param uri: URL to be tagged to **DELETE** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
2018-10-14 01:55:33 +01:00
return self.route(
uri,
methods=frozenset({"DELETE"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def add_route(
self,
handler,
uri,
methods=frozenset({"GET"}),
host=None,
strict_slashes=None,
version=None,
name=None,
stream=False,
):
"""A helper method to register class instance or
2016-11-25 07:29:25 +00:00
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed, these are overridden
if using a HTTPMethodView
:param host:
2017-08-21 11:05:34 +01:00
:param strict_slashes:
:param version:
:param name: user defined route name for url_for
2018-03-12 19:21:59 +00:00
:param stream: boolean specifying if the handler is a stream handler
:return: function or class instance
"""
# Handle HTTPMethodView differently
2018-10-14 01:55:33 +01:00
if hasattr(handler, "view_class"):
2017-02-02 21:24:16 +00:00
methods = set()
for method in HTTP_METHODS:
_handler = getattr(handler.view_class, method.lower(), None)
if _handler:
2017-02-02 21:24:16 +00:00
methods.add(method)
2018-10-14 01:55:33 +01:00
if hasattr(_handler, "is_stream"):
stream = True
2017-02-02 21:24:16 +00:00
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
for _handler in handler.handlers.values():
2018-10-14 01:55:33 +01:00
if hasattr(_handler, "is_stream"):
stream = True
break
2017-02-02 21:24:16 +00:00
if strict_slashes is None:
strict_slashes = self.strict_slashes
2018-10-14 01:55:33 +01:00
self.route(
uri=uri,
methods=methods,
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)(handler)
return handler
# Decorator
2018-10-14 01:55:33 +01:00
def websocket(
self,
uri,
host=None,
strict_slashes=None,
subprotocols=None,
version=None,
name=None,
2018-10-14 01:55:33 +01:00
):
"""
Decorate a function to be registered as a websocket route
:param uri: path of the URL
:param host: Host IP or FQDN details
:param strict_slashes: If the API endpoint needs to terminate
with a "/" or not
2018-10-26 09:29:53 +01:00
:param subprotocols: optional list of str with supported subprotocols
2019-05-12 13:36:13 +01:00
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: tuple of routes, decorated function
"""
self.enable_websocket()
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
2018-10-14 01:55:33 +01:00
if not uri.startswith("/"):
uri = "/" + uri
if strict_slashes is None:
strict_slashes = self.strict_slashes
def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
async def websocket_handler(request, *args, **kwargs):
2017-02-28 06:33:18 +00:00
request.app = self
if not getattr(handler, "__blueprintname__", False):
2017-10-17 07:11:56 +01:00
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "")
+ handler.__name__
)
2019-05-21 23:42:19 +01:00
pass
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
protocol = request.transport.get_protocol()
2019-05-21 23:42:19 +01:00
protocol.app = self
ws = await protocol.websocket_handshake(
request, subprotocols
)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
2017-10-11 12:02:26 +01:00
self.websocket_tasks.add(fut)
try:
await fut
except (CancelledError, ConnectionClosed):
pass
finally:
self.websocket_tasks.remove(fut)
await ws.close()
routes.extend(
self.router.add(
uri=uri,
handler=websocket_handler,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
2018-10-14 01:55:33 +01:00
)
return routes, handler
return response
2018-10-14 01:55:33 +01:00
def add_websocket_route(
self,
handler,
uri,
host=None,
strict_slashes=None,
subprotocols=None,
version=None,
2018-10-14 01:55:33 +01:00
name=None,
):
"""
A helper method to register a function as a websocket route.
:param handler: a callable function or instance of a class
that can handle the websocket request
:param host: Host IP or FQDN details
:param uri: URL path that will be mapped to the websocket
handler
handler
:param strict_slashes: If the API endpoint needs to terminate
with a "/" or not
:param subprotocols: Subprotocols to be used with websocket
handshake
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: Objected decorated by :func:`websocket`
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
2018-10-14 01:55:33 +01:00
return self.websocket(
uri,
host=host,
strict_slashes=strict_slashes,
subprotocols=subprotocols,
version=version,
2018-10-14 01:55:33 +01:00
name=name,
)(handler)
2017-02-20 22:32:14 +00:00
def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.
Websocket is enabled automatically if websocket routes are
added to the application.
"""
if not self.websocket_enabled:
# if the server is stopped, we want to cancel any ongoing
# websocket tasks, to allow the server to exit promptly
2018-10-14 01:55:33 +01:00
@self.listener("before_server_stop")
def cancel_websocket_tasks(app, loop):
for task in self.websocket_tasks:
task.cancel()
2017-02-20 22:32:14 +00:00
self.websocket_enabled = enable
2016-10-15 20:59:00 +01:00
# Decorator
def exception(self, *exceptions):
"""Decorate a function to be registered as a handler for exceptions
:param exceptions: exceptions
2016-10-15 20:59:00 +01:00
:return: decorated function
"""
def response(handler):
for exception in exceptions:
2017-03-08 00:22:23 +00:00
if isinstance(exception, (tuple, list)):
for e in exception:
self.error_handler.add(e, handler)
else:
self.error_handler.add(exception, handler)
2016-10-15 20:59:00 +01:00
return handler
return response
2018-10-14 01:55:33 +01:00
def register_middleware(self, middleware, attach_to="request"):
"""
Register an application level middleware that will be attached
to all the API URLs registered under this application.
This method is internally invoked by the :func:`middleware`
decorator provided at the app level.
:param middleware: Callback method to be attached to the
middleware
:param attach_to: The state at which the middleware needs to be
invoked in the lifecycle of an *HTTP Request*.
**request** - Invoke before the request is processed
**response** - Invoke before the response is returned back
:return: decorated method
"""
2018-10-14 01:55:33 +01:00
if attach_to == "request":
Enable Middleware Support for Blueprint Groups (#1399) * enable blueprint group middleware support This commit will enable the users to implement a middleware at the blueprint group level whereby enforcing the middleware automatically to each of the available Blueprints that are part of the group. This will eanble a simple way in which a certain set of common features and criteria can be enforced on a Blueprint group. i.e. authentication and authorization This commit will address the feature request raised as part of Issue #1386 Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * enable indexing of BlueprintGroup object Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * rename blueprint group file to fix spelling error Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add documentation and additional unit tests Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup and optimize headers in unit test file Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix Bluprint Group iteratable method Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add additional unit test to check StopIteration condition Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup iter protocol implemenation for blueprint group and add slots Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix blueprint group middleware invocation identification Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * feat: enable list behavior on blueprint group object and use append instead of properly to add blueprint to group Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-03 22:26:05 +00:00
if middleware not in self.request_middleware:
self.request_middleware.append(middleware)
2018-10-14 01:55:33 +01:00
if attach_to == "response":
Enable Middleware Support for Blueprint Groups (#1399) * enable blueprint group middleware support This commit will enable the users to implement a middleware at the blueprint group level whereby enforcing the middleware automatically to each of the available Blueprints that are part of the group. This will eanble a simple way in which a certain set of common features and criteria can be enforced on a Blueprint group. i.e. authentication and authorization This commit will address the feature request raised as part of Issue #1386 Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * enable indexing of BlueprintGroup object Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * rename blueprint group file to fix spelling error Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add documentation and additional unit tests Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup and optimize headers in unit test file Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix Bluprint Group iteratable method Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add additional unit test to check StopIteration condition Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup iter protocol implemenation for blueprint group and add slots Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix blueprint group middleware invocation identification Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * feat: enable list behavior on blueprint group object and use append instead of properly to add blueprint to group Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-03 22:26:05 +00:00
if middleware not in self.response_middleware:
self.response_middleware.appendleft(middleware)
return middleware
def register_named_middleware(
self, middleware, route_names, attach_to="request"
):
if attach_to == "request":
for _rn in route_names:
if _rn not in self.named_request_middleware:
self.named_request_middleware[_rn] = deque()
if middleware not in self.named_request_middleware[_rn]:
self.named_request_middleware[_rn].append(middleware)
if attach_to == "response":
for _rn in route_names:
if _rn not in self.named_response_middleware:
self.named_response_middleware[_rn] = deque()
if middleware not in self.named_response_middleware[_rn]:
self.named_response_middleware[_rn].appendleft(middleware)
2016-10-15 20:59:00 +01:00
# Decorator
2017-02-11 12:27:25 +00:00
def middleware(self, middleware_or_request):
2016-10-15 20:59:00 +01:00
"""
Decorate and register middleware to be called before a request.
Can either be called as *@app.middleware* or
*@app.middleware('request')*
2016-10-15 20:59:00 +01:00
:param: middleware_or_request: Optional parameter to use for
identifying which type of middleware is being registered.
"""
2016-10-15 20:59:00 +01:00
# Detect which way this was called, @middleware or @middleware('AT')
2017-02-11 12:27:25 +00:00
if callable(middleware_or_request):
return self.register_middleware(middleware_or_request)
2017-02-11 12:27:25 +00:00
2016-10-15 20:59:00 +01:00
else:
2018-10-14 01:55:33 +01:00
return partial(
self.register_middleware, attach_to=middleware_or_request
)
2016-10-15 20:59:00 +01:00
# Static Files
2018-10-14 01:55:33 +01:00
def static(
self,
uri,
file_or_directory,
pattern=r"/?.+",
use_modified_since=True,
use_content_range=False,
stream_large_files=False,
name="static",
host=None,
strict_slashes=None,
content_type=None,
):
"""
Register a root to serve files from. The input can either be a
file or a directory. This method will enable an easy and simple way
to setup the :class:`Route` necessary to serve the static files.
:param uri: URL path to be used for serving static content
:param file_or_directory: Path for the Static file/directory with
static files
:param pattern: Regex Pattern identifying the valid static files
:param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's
:param use_content_range: If true, process header for range requests
and sends the file part that is requested
:param stream_large_files: If true, use the
:func:`StreamingHTTPResponse.file_stream` handler rather
than the :func:`HTTPResponse.file` handler to send the file.
If this is an integer, this represents the threshold size to
switch to :func:`StreamingHTTPResponse.file_stream`
:param name: user defined name used for url_for
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param content_type: user defined content type for header
:return: None
"""
2018-10-14 01:55:33 +01:00
static_register(
self,
uri,
file_or_directory,
pattern,
use_modified_since,
use_content_range,
stream_large_files,
name,
host,
strict_slashes,
content_type,
)
def blueprint(self, blueprint, **options):
"""Register a blueprint on the application.
2018-01-19 01:20:51 +00:00
:param blueprint: Blueprint object or (list, tuple) thereof
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
Enable Middleware Support for Blueprint Groups (#1399) * enable blueprint group middleware support This commit will enable the users to implement a middleware at the blueprint group level whereby enforcing the middleware automatically to each of the available Blueprints that are part of the group. This will eanble a simple way in which a certain set of common features and criteria can be enforced on a Blueprint group. i.e. authentication and authorization This commit will address the feature request raised as part of Issue #1386 Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * enable indexing of BlueprintGroup object Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * rename blueprint group file to fix spelling error Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add documentation and additional unit tests Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup and optimize headers in unit test file Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix Bluprint Group iteratable method Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * add additional unit test to check StopIteration condition Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * cleanup iter protocol implemenation for blueprint group and add slots Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix blueprint group middleware invocation identification Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * feat: enable list behavior on blueprint group object and use append instead of properly to add blueprint to group Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-03 22:26:05 +00:00
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
2018-01-19 01:20:51 +00:00
for item in blueprint:
self.blueprint(item, **options)
return
if blueprint.name in self.blueprints:
2018-10-14 01:55:33 +01:00
assert self.blueprints[blueprint.name] is blueprint, (
'A blueprint with the name "%s" is already registered. '
"Blueprint names must be unique." % (blueprint.name,)
)
else:
self.blueprints[blueprint.name] = blueprint
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
"""
Proxy method provided for invoking the :func:`blueprint` method
.. note::
To be deprecated in 1.0. Use :func:`blueprint` instead.
:param args: Blueprint object or (list, tuple) thereof
:param kwargs: option dictionary with blueprint defaults
:return: None
"""
2017-01-28 01:59:08 +00:00
if self.debug:
2018-10-14 01:55:33 +01:00
warnings.simplefilter("default")
warnings.warn(
"Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method"
" instead",
DeprecationWarning,
)
return self.blueprint(*args, **kwargs)
2017-02-02 17:21:14 +00:00
def url_for(self, view_name: str, **kwargs):
2018-10-26 09:29:53 +01:00
r"""Build a URL based on a view name and the values provided.
2017-02-02 17:52:48 +00:00
In order to build a URL, all request parameters must be supplied as
keyword arguments, and each parameter must pass the test for the
specified parameter type. If these conditions are not met, a
`URLBuildError` will be thrown.
Keyword arguments that are not request parameters will be included in
the output URL's query string.
:param view_name: string referencing the view name
2018-10-26 09:29:53 +01:00
:param \**kwargs: keys and values that are used to build request
2017-02-02 17:52:48 +00:00
parameters and query string arguments.
:return: the built URL
Raises:
URLBuildError
"""
# find the route by the supplied view name
kw: Dict[str, str] = {}
# special static files url_for
2018-10-14 01:55:33 +01:00
if view_name == "static":
kw.update(name=kwargs.pop("name", "static"))
elif view_name.endswith(".static"): # blueprint.static
kwargs.pop("name", None)
kw.update(name=view_name)
uri, route = self.router.find_route_by_view_name(view_name, **kw)
if not (uri and route):
2018-10-14 01:55:33 +01:00
raise URLBuildError(
f"Endpoint with name `{view_name}` was not found"
2018-10-14 01:55:33 +01:00
)
2017-02-02 17:21:14 +00:00
# If the route has host defined, split that off
# TODO: Retain netloc and path separately in Route objects
host = uri.find("/")
if host > 0:
host, uri = uri[:host], uri[host:]
else:
host = None
2018-10-14 01:55:33 +01:00
if view_name == "static" or view_name.endswith(".static"):
filename = kwargs.pop("filename", None)
# it's static folder
2018-10-14 01:55:33 +01:00
if "<file_uri:" in uri:
folder_ = uri.split("<file_uri:", 1)[0]
if folder_.endswith("/"):
folder_ = folder_[:-1]
2018-10-14 01:55:33 +01:00
if filename.startswith("/"):
filename = filename[1:]
uri = f"{folder_}/{filename}"
2018-10-14 01:55:33 +01:00
if uri != "/" and uri.endswith("/"):
2017-02-20 23:58:27 +00:00
uri = uri[:-1]
2017-02-02 17:21:14 +00:00
out = uri
2017-02-02 17:52:48 +00:00
# find all the parameters we will need to build in the URL
2018-10-14 01:55:33 +01:00
matched_params = re.findall(self.router.parameter_pattern, uri)
2017-02-02 17:21:14 +00:00
# _method is only a placeholder now, don't know how to support it
2018-10-14 01:55:33 +01:00
kwargs.pop("_method", None)
anchor = kwargs.pop("_anchor", "")
# _external need SERVER_NAME in config or pass _server arg
2018-10-14 01:55:33 +01:00
external = kwargs.pop("_external", False)
scheme = kwargs.pop("_scheme", "")
if scheme and not external:
2018-10-14 01:55:33 +01:00
raise ValueError("When specifying _scheme, _external must be True")
2018-10-14 01:55:33 +01:00
netloc = kwargs.pop("_server", None)
if netloc is None and external:
netloc = host or self.config.get("SERVER_NAME", "")
if external:
if not scheme:
2018-10-14 01:55:33 +01:00
if ":" in netloc[:8]:
scheme = netloc[:8].split(":", 1)[0]
2017-09-27 02:59:49 +01:00
else:
2018-10-14 01:55:33 +01:00
scheme = "http"
2018-10-14 01:55:33 +01:00
if "://" in netloc[:8]:
netloc = netloc.split("://", 1)[-1]
2017-02-02 17:21:14 +00:00
for match in matched_params:
2018-10-14 01:55:33 +01:00
name, _type, pattern = self.router.parse_parameter_string(match)
2017-02-02 17:52:48 +00:00
# we only want to match against each individual parameter
specific_pattern = f"^{pattern}$"
2017-02-02 17:21:14 +00:00
supplied_param = None
2017-02-02 17:52:48 +00:00
if name in kwargs:
2017-02-02 17:21:14 +00:00
supplied_param = kwargs.get(name)
del kwargs[name]
else:
raise URLBuildError(
f"Required parameter `{name}` was not passed to url_for"
2018-10-14 01:55:33 +01:00
)
2017-02-02 17:21:14 +00:00
supplied_param = str(supplied_param)
2017-02-02 17:52:48 +00:00
# determine if the parameter supplied by the caller passes the test
# in the URL
2017-02-02 17:21:14 +00:00
passes_pattern = re.match(specific_pattern, supplied_param)
if not passes_pattern:
if _type != str:
type_name = _type.__name__
2017-02-02 17:21:14 +00:00
msg = (
f'Value "{supplied_param}" '
f"for parameter `{name}` does not "
f"match pattern for type `{type_name}`: {pattern}"
2018-10-14 01:55:33 +01:00
)
2017-02-02 17:21:14 +00:00
else:
msg = (
f'Value "{supplied_param}" for parameter `{name}` '
f"does not satisfy pattern {pattern}"
2018-10-14 01:55:33 +01:00
)
2017-02-02 17:21:14 +00:00
raise URLBuildError(msg)
2017-02-02 17:52:48 +00:00
# replace the parameter in the URL with the supplied value
replacement_regex = f"(<{name}.*?>)"
2017-02-02 17:21:14 +00:00
2018-10-14 01:55:33 +01:00
out = re.sub(replacement_regex, supplied_param, out)
2017-02-02 17:21:14 +00:00
# parse the remainder of the keyword arguments into a querystring
2018-10-14 01:55:33 +01:00
query_string = urlencode(kwargs, doseq=True) if kwargs else ""
# scheme://netloc/path;parameters?query#fragment
2018-10-14 01:55:33 +01:00
out = urlunparse((scheme, netloc, out, "", query_string, anchor))
2017-02-02 17:21:14 +00:00
return out
2016-10-15 20:59:00 +01:00
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #
def converted_response_type(self, response):
"""
No implementation provided.
"""
pass
2017-02-21 16:28:45 +00:00
async def handle_request(self, request, write_callback, stream_callback):
"""Take a request from the HTTP Server and return a response object
to be sent back The HTTP Server only expects a response object, so
2016-10-16 14:01:59 +01:00
exception handling must be done here
2016-10-15 20:59:00 +01:00
:param request: HTTP Request object
2017-02-21 16:28:45 +00:00
:param write_callback: Synchronous response function to be
called with the response as the only argument
:param stream_callback: Coroutine that handles streaming a
StreamingHTTPResponse if produced by the handler.
2016-10-15 20:59:00 +01:00
:return: Nothing
"""
# Define `response` var here to remove warnings about
# allocation before assignment below.
response = None
cancelled = False
name = None
2016-10-15 20:59:00 +01:00
try:
# Fetch handler from router
handler, args, kwargs, uri, name = self.router.get(request)
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
response = await self._run_request_middleware(
request, request_name=name
)
2016-10-15 20:59:00 +01:00
# No middleware results
if not response:
# -------------------------------------------- #
# Execute Handler
# -------------------------------------------- #
2017-04-28 20:06:59 +01:00
request.uri_template = uri
2016-10-15 20:59:00 +01:00
if handler is None:
2016-10-16 14:01:59 +01:00
raise ServerError(
2018-10-14 01:55:33 +01:00
(
"'None' was returned while requesting a "
"handler from the router"
)
)
else:
if not getattr(handler, "__blueprintname__", False):
request.endpoint = self._build_endpoint_name(
handler.__name__
)
else:
request.endpoint = self._build_endpoint_name(
getattr(handler, "__blueprintname__", ""),
handler.__name__,
)
2016-10-15 20:59:00 +01:00
# Run response handler
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
except CancelledError:
# If response handler times out, the server handles the error
# and cancels the handle_request job.
# In this case, the transport is already closed and we cannot
# issue a response.
response = None
cancelled = True
2016-10-15 20:59:00 +01:00
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
# -------------------------------------------- #
2016-10-15 20:59:00 +01:00
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
2018-01-05 22:12:22 +00:00
if isinstance(e, SanicException):
2018-10-14 01:55:33 +01:00
response = self.error_handler.default(
request=request, exception=e
)
2018-01-05 22:12:22 +00:00
elif self.debug:
2016-10-16 14:01:59 +01:00
response = HTTPResponse(
f"Error while "
f"handling error: {e}\nStack: {format_exc()}",
2018-10-14 01:55:33 +01:00
status=500,
)
2016-10-15 20:59:00 +01:00
else:
2016-10-16 14:01:59 +01:00
response = HTTPResponse(
2018-10-14 01:55:33 +01:00
"An error occurred while handling an error", status=500
)
finally:
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
# Don't run response middleware if response is None
if response is not None:
try:
2018-10-14 01:55:33 +01:00
response = await self._run_response_middleware(
request, response, request_name=name
2018-10-14 01:55:33 +01:00
)
except CancelledError:
# Response middleware can timeout too, as above.
response = None
cancelled = True
except BaseException:
error_logger.exception(
2018-10-14 01:55:33 +01:00
"Exception occurred in one of response "
"middleware handlers"
)
if cancelled:
raise CancelledError()
2017-02-21 16:28:45 +00:00
# pass the response to the correct callback
if write_callback is None or isinstance(
response, StreamingHTTPResponse
):
2019-05-21 23:42:19 +01:00
if stream_callback:
await stream_callback(response)
else:
2019-05-26 22:57:50 +01:00
# Should only end here IF it is an ASGI websocket.
2019-05-21 23:42:19 +01:00
# TODO:
# - Add exception handling
pass
2017-02-21 16:28:45 +00:00
else:
write_callback(response)
2016-10-15 20:59:00 +01:00
2017-02-14 19:51:20 +00:00
# -------------------------------------------------------------------- #
# Testing
# -------------------------------------------------------------------- #
@property
def test_client(self):
2017-03-08 00:39:26 +00:00
return SanicTestClient(self)
2017-02-14 19:51:20 +00:00
@property
def asgi_client(self):
return SanicASGITestClient(self)
2016-10-15 20:59:00 +01:00
# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
2018-10-14 01:55:33 +01:00
def run(
self,
2019-01-02 22:24:58 +00:00
host: Optional[str] = None,
port: Optional[int] = None,
*,
2019-01-02 00:35:25 +00:00
debug: bool = False,
auto_reload: Optional[bool] = None,
2019-01-02 22:24:58 +00:00
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
2019-01-02 00:35:25 +00:00
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
2019-01-02 00:35:25 +00:00
backlog: int = 100,
2019-01-02 22:24:58 +00:00
stop_event: Any = None,
2019-01-02 00:35:25 +00:00
register_sys_signals: bool = True,
2019-01-02 22:24:58 +00:00
access_log: Optional[bool] = None,
loop: None = None,
) -> None:
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
2016-10-15 20:59:00 +01:00
:param host: Address to host on
:type host: str
2016-10-15 20:59:00 +01:00
:param port: Port to host on
:type port: int
2016-10-15 20:59:00 +01:00
:param debug: Enables debug output (slows server)
:type debug: bool
:param auto_reload: Reload app whenever its source code is changed.
Enabled by default in debug mode.
:type auto_relaod: bool
2017-03-08 03:54:02 +00:00
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
2016-10-18 09:22:49 +01:00
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
:type workers: int
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
2019-01-02 00:35:25 +00:00
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
2016-10-15 20:59:00 +01:00
:return: Nothing
"""
if loop is not None:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
"#asynchronous-support"
)
if auto_reload or auto_reload is None and debug:
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0)
2017-09-13 07:42:42 +01:00
2017-05-23 04:28:12 +01:00
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
2018-10-14 01:55:33 +01:00
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
2017-02-27 00:31:39 +00:00
if stop_event is not None:
if debug:
2018-10-14 01:55:33 +01:00
warnings.simplefilter("default")
warnings.warn(
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
2019-01-02 00:35:25 +00:00
self.config.ACCESS_LOG = access_log
2017-01-29 22:01:00 +00:00
server_settings = self._helper(
2018-10-14 01:55:33 +01:00
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
workers=workers,
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
auto_reload=auto_reload,
)
2016-10-15 20:59:00 +01:00
try:
self.is_running = True
Fix Ctrl+C and tests on Windows. (#1808) * Fix Ctrl+C on Windows. * Disable testing of a function N/A on Windows. * Add test for coverage, avoid crash on missing _stopping. * Initialise StreamingHTTPResponse.protocol = None * Improved comments. * Reduce amount of data in test_request_stream to avoid failures on Windows. * The Windows test doesn't work on Windows :( * Use port numbers more likely to be free than 8000. * Disable the other signal tests on Windows as well. * Windows doesn't properly support SO_REUSEADDR, so that's disabled in Python, and thus rebinding fails. For successful testing, reuse port instead. * app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests * Revert "app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests" This reverts commit dc5d682448e3f6595bdca5cb764e5f26ca29e295. * Use random test server port on most tests. Should avoid port/addr reuse issues. * Another test to random port instead of 8000. * Fix deprecation warnings about missing name on Sanic() in tests. * Linter and typing * Increase test coverage * Rewrite test for ctrlc_windows_workaround * py36 compat * py36 compat * py36 compat * Don't rely on loop internals but add a stopping flag to app. * App may be restarted. * py36 compat * Linter * Add a constant for OS checking. Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-26 04:42:46 +00:00
self.is_stopping = False
if workers > 1 and os.name != "posix":
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
workers = 1
2016-10-18 09:22:49 +01:00
if workers == 1:
serve(**server_settings)
2016-10-18 09:22:49 +01:00
else:
2017-02-27 00:31:39 +00:00
serve_multiple(server_settings, workers)
2017-10-26 05:58:31 +01:00
except BaseException:
2017-09-12 06:12:49 +01:00
error_logger.exception(
2018-10-14 01:55:33 +01:00
"Experienced exception while trying to serve"
)
raise
finally:
self.is_running = False
2017-09-12 06:12:49 +01:00
logger.info("Server Stopped")
2016-10-18 09:22:49 +01:00
2016-10-15 20:59:00 +01:00
def stop(self):
"""This kills the Sanic"""
Fix Ctrl+C and tests on Windows. (#1808) * Fix Ctrl+C on Windows. * Disable testing of a function N/A on Windows. * Add test for coverage, avoid crash on missing _stopping. * Initialise StreamingHTTPResponse.protocol = None * Improved comments. * Reduce amount of data in test_request_stream to avoid failures on Windows. * The Windows test doesn't work on Windows :( * Use port numbers more likely to be free than 8000. * Disable the other signal tests on Windows as well. * Windows doesn't properly support SO_REUSEADDR, so that's disabled in Python, and thus rebinding fails. For successful testing, reuse port instead. * app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests * Revert "app.run argument handling: added server kwargs (alike create_server), added warning on extra kwargs, made auto_reload explicit argument. Another go at Windows tests" This reverts commit dc5d682448e3f6595bdca5cb764e5f26ca29e295. * Use random test server port on most tests. Should avoid port/addr reuse issues. * Another test to random port instead of 8000. * Fix deprecation warnings about missing name on Sanic() in tests. * Linter and typing * Increase test coverage * Rewrite test for ctrlc_windows_workaround * py36 compat * py36 compat * py36 compat * Don't rely on loop internals but add a stopping flag to app. * App may be restarted. * py36 compat * Linter * Add a constant for OS checking. Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2020-03-26 04:42:46 +00:00
if not self.is_stopping:
self.is_stopping = True
get_event_loop().stop()
2016-10-18 09:22:49 +01:00
2018-10-14 01:55:33 +01:00
async def create_server(
self,
2019-01-02 22:24:58 +00:00
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
2019-01-02 22:24:58 +00:00
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
2019-01-02 22:24:58 +00:00
stop_event: Any = None,
access_log: Optional[bool] = None,
return_asyncio_server=False,
asyncio_server_kwargs=None,
) -> Optional[AsyncioServer]:
"""
Asynchronous version of :func:`run`.
This method will take care of the operations necessary to invoke
the *before_start* events via :func:`trigger_events` method invocation
before starting the *sanic* app in Async mode.
.. note::
This does not support multiprocessing and is not the preferred
way to run a :class:`Sanic` application.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
2019-01-02 00:35:25 +00:00
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
to return asyncio.Server or
start it serving right away
:type return_asyncio_server: bool
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:return: AsyncioServer if return_asyncio_server is true, else Nothing
"""
2017-09-13 07:42:42 +01:00
2017-05-23 04:28:12 +01:00
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
2018-10-14 01:55:33 +01:00
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
2017-02-27 00:31:39 +00:00
if stop_event is not None:
if debug:
2018-10-14 01:55:33 +01:00
warnings.simplefilter("default")
warnings.warn(
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
2019-01-02 00:35:25 +00:00
self.config.ACCESS_LOG = access_log
2017-01-29 22:01:00 +00:00
server_settings = self._helper(
2018-10-14 01:55:33 +01:00
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
run_async=return_asyncio_server,
2018-10-14 01:55:33 +01:00
)
# Trigger before_start events
2017-08-09 06:21:40 +01:00
await self.trigger_events(
2018-10-14 01:55:33 +01:00
server_settings.get("before_start", []),
server_settings.get("loop"),
2017-08-09 06:21:40 +01:00
)
return await serve(
2019-01-15 15:48:26 +00:00
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
async def trigger_events(self, events, loop):
"""Trigger events (functions or async)
:param events: one or more sync or async functions to execute
:param loop: event loop
"""
for event in events:
result = event(loop)
if isawaitable(result):
await result
async def _run_request_middleware(self, request, request_name=None):
# The if improves speed. I don't know why
named_middleware = self.named_request_middleware.get(
request_name, deque()
)
applicable_middleware = self.request_middleware + named_middleware
if applicable_middleware:
for middleware in applicable_middleware:
response = middleware(request)
if isawaitable(response):
response = await response
if response:
return response
return None
async def _run_response_middleware(
self, request, response, request_name=None
):
named_middleware = self.named_response_middleware.get(
request_name, deque()
)
applicable_middleware = self.response_middleware + named_middleware
if applicable_middleware:
for middleware in applicable_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
return response
2018-10-14 01:55:33 +01:00
def _helper(
self,
host=None,
port=None,
debug=False,
ssl=None,
sock=None,
workers=1,
loop=None,
protocol=HttpProtocol,
backlog=100,
stop_event=None,
register_sys_signals=True,
run_async=False,
auto_reload=False,
):
"""Helper function used by `run` and `create_server`."""
2017-03-08 03:54:02 +00:00
if isinstance(ssl, dict):
# try common aliaseses
2018-10-14 01:55:33 +01:00
cert = ssl.get("cert") or ssl.get("certificate")
key = ssl.get("key") or ssl.get("keyfile")
2017-04-08 21:31:11 +01:00
if cert is None or key is None:
2017-03-08 03:54:02 +00:00
raise ValueError("SSLContext or certificate and key required.")
2017-04-08 21:31:11 +01:00
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
2017-03-08 03:54:02 +00:00
context.load_cert_chain(cert, keyfile=key)
ssl = context
2017-02-27 00:31:39 +00:00
if stop_event is not None:
if debug:
2018-10-14 01:55:33 +01:00
warnings.simplefilter("default")
warnings.warn(
"stop_event will be removed from future versions.",
DeprecationWarning,
)
Forwarded headers and otherwise improved proxy handling (#1638) * Added support for HTTP Forwarded header and combined parsing of other proxy headers. - Accessible via request.forwarded that tries parse_forwarded and then parse_xforwarded - parse_forwarded uses the Forwarded header, if config.FORWARDED_SECRET is provided and a matching header field is found - parse_xforwarded uses X-Real-IP and X-Forwarded-* much alike the existing implementation - This commit does not change existing request properties that still use the old code and won't make use of Forwarded headers. * Use req.forwarded in req properties server_name, server_port, scheme and remote_addr. X-Scheme handling moved to parse_xforwarded. * Cleanup and fix req.server_port; no longer reports socket port if any forwards headers are used. * Update docstrings to incidate that forwarded header is used first. * Remove testing function. * Fix tests and linting. - One test removed due to change of semantics - no socket port will be used if any forwarded headers are in effect. - Other tests augmented with X-Forwarded-For, to allow the header being tested take effect (shouldn't affect old implementation). * Try to workaround buggy tools complaining about incorrect ordering of imports. * Cleanup forwarded processing, add comments. secret is now also returned. * Added tests, fixed quoted string handling, cleanup. * Further tests for full coverage. * Try'n make linter happy. * Add support for multiple Forwarded headers. Unify parse_forwarded parameters with parse_xforwarded. * Implement multiple headers support for X-Forwarded-For. - Previously only the first header was used, so this BUGFIX may affect functionality. * Bugfix for request.server_name: strip port and other parts. - request.server_name docs claim that it returns the hostname only (no port). - config.SERVER_NAME may be full URL, so strip scheme, port and path - HTTP Host and consequently forwarded Host may include port number, so strip that also for forwarded hosts (previously only done for HTTP Host). - Possible performance benefit of limiting to one split. * Fallback to app.url_for and let it handle SERVER_NAME if defined (until a proper solution is implemented). * Revise previous commit. Only fallback for full URL SERVER_NAMEs; allows host to be defined and proxied information still being used. * Heil lintnazi. * Modify testcase not to use underscores in URLs. Use hyphens which the spec allows for. * Forwarded and Host header parsing improved. - request.forwarded lowercases hosts, separates host:port into their own fields and lowercases addresses - forwarded.parse_host helper function added and used for parsing all host-style headers (IPv6 cannot be simply split(":")). - more tests fixed not to use underscores in hosts as those are no longer accepted and lead to the field being rejected * Fixed typo in docstring. * Added IPv6 address tests for Host header. * Fix regex. * Further tests and stricter forwarded handling. * Fix merge commit * Linter * Linter * Linter * Add to avoid re-using the variable. Make a few raw strings non-raw. * Remove unnecessary or * Updated docs (work in progress). * Enable REAL_IP_HEADER parsing irregardless of PROXIES_COUNT setting. - Also cleanup and added comments * New defaults for PROXIES_COUNT and REAL_IP_HEADER, updated tests. * Remove support for PROXIES_COUNT=-1. * Linter errors. - This is getting ridiculous: cannot fit an URL on one line, linter requires splitting the string literal! * Add support for by=_proxySecret, updated docs, updated tests. * Forwarded headers' semantics tuning. - Forwarded host is now preserved in original format - request.host now returns a forwarded host if available, else the Host header - Forwarded options are preserved in original order, and later keys override earlier ones - Forwarded path is automatically URL-unquoted - Forwarded 'by' and 'for' are omitted if their value is unknown - Tests modified accordingly - Cleanup and improved documentation * Add ASGI test. * Linter * Linter #2
2019-09-02 14:50:56 +01:00
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
"#proxy-configuration"
)
2017-01-28 01:59:08 +00:00
self.error_handler.debug = debug
self.debug = debug
2017-01-24 03:58:37 +00:00
server_settings = {
2018-10-14 01:55:33 +01:00
"protocol": protocol,
"host": host,
"port": port,
"sock": sock,
"ssl": ssl,
"app": self,
2018-10-14 01:55:33 +01:00
"signal": Signal(),
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
2017-01-24 03:58:37 +00:00
}
# -------------------------------------------- #
# Register start/stop events
# -------------------------------------------- #
for event_name, settings_name, reverse in (
2018-10-14 01:55:33 +01:00
("before_server_start", "before_start", False),
("after_server_start", "after_start", False),
("before_server_stop", "before_stop", True),
("after_server_stop", "after_stop", True),
):
listeners = self.listeners[event_name].copy()
2017-01-24 03:58:37 +00:00
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
if self.configure_logging and debug:
2017-09-12 06:12:49 +01:00
logger.setLevel(logging.DEBUG)
2017-12-07 13:00:54 +00:00
2018-10-14 01:55:33 +01:00
if (
self.config.LOGO
2018-10-14 01:55:33 +01:00
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
logger.debug(
self.config.LOGO
if isinstance(self.config.LOGO, str)
else BASE_LOGO
)
2017-01-24 03:58:37 +00:00
2017-01-29 22:01:00 +00:00
if run_async:
2018-10-14 01:55:33 +01:00
server_settings["run_async"] = True
2017-01-24 03:58:37 +00:00
# Serve
if host and port:
2017-03-12 08:19:34 +00:00
proto = "http"
if ssl is not None:
proto = "https"
logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
2017-01-24 03:58:37 +00:00
return server_settings
def _build_endpoint_name(self, *parts):
parts = [self.name, *parts]
return ".".join(parts)
2019-01-18 14:50:42 +00:00
# -------------------------------------------------------------------- #
# ASGI
# -------------------------------------------------------------------- #
2019-01-18 14:50:42 +00:00
async def __call__(self, scope, receive, send):
2019-06-18 07:57:42 +01:00
"""To be ASGI compliant, our instance must be a callable that accepts
three arguments: scope, receive, send. See the ASGI reference for more
details: https://asgi.readthedocs.io/en/latest/"""
2019-06-04 08:58:00 +01:00
self.asgi = True
2019-05-21 23:42:19 +01:00
asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app()