From b7d4121586e779bd40a1cfd2188b8180d4486ab5 Mon Sep 17 00:00:00 2001 From: Lagicrus Date: Wed, 11 Sep 2019 22:37:14 +0100 Subject: [PATCH 01/19] Update static_files.md (#1672) --- docs/sanic/static_files.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sanic/static_files.md b/docs/sanic/static_files.md index d3075238..601f359c 100644 --- a/docs/sanic/static_files.md +++ b/docs/sanic/static_files.md @@ -34,6 +34,10 @@ app.url_for('static', name='another', filename='any') == '/another.png' bp = Blueprint('bp', url_prefix='/bp') bp.static('/static', './static') +# specify a different content_type for your files +# such as adding 'charset' +app.static('/', '/public/index.html', content_type="text/html; charset=utf-8") + # servers the file directly bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') app.blueprint(bp) From e13f42c17b08b8ac5caa342d6e8d81e0981067a8 Mon Sep 17 00:00:00 2001 From: ku-mu Date: Tue, 17 Sep 2019 02:27:22 +0900 Subject: [PATCH 02/19] Fix docstring style in Sanic.register_listener (#1678) * Fix docstring style: google -> reST --- sanic/app.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index c2e44a06..92e1a8dd 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -138,11 +138,9 @@ class Sanic: """ Register the listener for a given event. - Args: - listener: callable i.e. setup_db(app, loop) - event: when to register listener i.e. 'before_server_start' - - Returns: listener + :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) From 7674e917e446215466cd6464ad875f3e13b06af2 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Tue, 17 Sep 2019 03:59:16 +1000 Subject: [PATCH 03/19] Fixes "after_server_start" when using return_asyncio_server. (#1676) * Fixes ability to trigger "after_server_start", "before_server_stop", "after_server_stop" server events when using app.create_server to start your own asyncio_server See example file run_async_advanced for a full example * Fix a missing method on AsyncServer that some tests need Add a tiny bit more documentation in-code Change name of AsyncServerCoro to AsyncioServer --- docs/sanic/deploying.md | 31 +++++++++++++ examples/run_async_advanced.py | 38 ++++++++++++++++ sanic/server.py | 83 +++++++++++++++++++++++++++++++++- tests/test_server_events.py | 50 ++++++++++++++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 examples/run_async_advanced.py diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md index 34b64a12..b72750db 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.md @@ -157,4 +157,35 @@ server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True loop = asyncio.get_event_loop() task = asyncio.ensure_future(server) loop.run_forever() +``` + +Caveat: using this method, calling `app.create_server()` will trigger "before_server_start" server events, but not +"after_server_start", "before_server_stop", or "after_server_stop" server events. + +For more advanced use-cases, you can trigger these events using the AsyncioServer object, returned by awaiting +the server task. + +Here is an incomplete example (please see `run_async_advanced.py` in examples for something more complete): + +```python +serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True) +loop = asyncio.get_event_loop() +serv_task = asyncio.ensure_future(serv_coro, loop=loop) +server = loop.run_until_complete(serv_task) +server.after_start() +try: + loop.run_forever() +except KeyboardInterrupt as e: + loop.stop() +finally: + server.before_stop() + + # Wait for server to close + close_task = server.close() + loop.run_until_complete(close_task) + + # Complete all tasks on the loop + for connection in server.connections: + connection.close_if_idle() + server.after_stop() ``` \ No newline at end of file diff --git a/examples/run_async_advanced.py b/examples/run_async_advanced.py new file mode 100644 index 00000000..36027c2f --- /dev/null +++ b/examples/run_async_advanced.py @@ -0,0 +1,38 @@ +from sanic import Sanic +from sanic import response +from signal import signal, SIGINT +import asyncio +import uvloop + +app = Sanic(__name__) + +@app.listener('after_server_start') +async def after_start_test(app, loop): + print("Async Server Started!") + +@app.route("/") +async def test(request): + return response.json({"answer": "42"}) + +asyncio.set_event_loop(uvloop.new_event_loop()) +serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True) +loop = asyncio.get_event_loop() +serv_task = asyncio.ensure_future(serv_coro, loop=loop) +signal(SIGINT, lambda s, f: loop.stop()) +server = loop.run_until_complete(serv_task) +server.after_start() +try: + loop.run_forever() +except KeyboardInterrupt as e: + loop.stop() +finally: + server.before_stop() + + # Wait for server to close + close_task = server.close() + loop.run_until_complete(close_task) + + # Complete all tasks on the loop + for connection in server.connections: + connection.close_if_idle() + server.after_stop() diff --git a/sanic/server.py b/sanic/server.py index 03362395..f18a27d7 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -634,6 +634,78 @@ def trigger_events(events, loop): loop.run_until_complete(result) +class AsyncioServer: + """ + Wraps an asyncio server with functionality that might be useful to + a user who needs to manage the server lifecycle manually. + """ + + __slots__ = ( + "loop", + "serve_coro", + "_after_start", + "_before_stop", + "_after_stop", + "server", + "connections", + ) + + def __init__( + self, + loop, + serve_coro, + connections, + after_start, + before_stop, + after_stop, + ): + # Note, Sanic already called "before_server_start" events + # before this helper was even created. So we don't need it here. + self.loop = loop + self.serve_coro = serve_coro + self._after_start = after_start + self._before_stop = before_stop + self._after_stop = after_stop + self.server = None + self.connections = connections + + def after_start(self): + """Trigger "after_server_start" events""" + trigger_events(self._after_start, self.loop) + + def before_stop(self): + """Trigger "before_server_stop" events""" + trigger_events(self._before_stop, self.loop) + + def after_stop(self): + """Trigger "after_server_stop" events""" + trigger_events(self._after_stop, self.loop) + + def is_serving(self): + if self.server: + return self.server.is_serving() + return False + + def wait_closed(self): + if self.server: + return self.server.wait_closed() + + def close(self): + if self.server: + self.server.close() + coro = self.wait_closed() + task = asyncio.ensure_future(coro, loop=self.loop) + return task + + def __await__(self): + """Starts the asyncio server, returns AsyncServerCoro""" + task = asyncio.ensure_future(self.serve_coro) + while not task.done(): + yield + self.server = task.result() + return self + + def serve( host, port, @@ -700,6 +772,8 @@ def serve( :param reuse_port: `True` for multiple workers :param loop: asyncio compatible event loop :param protocol: subclass of asyncio protocol class + :param run_async: bool: Do not create a new event loop for the server, + and return an AsyncServer object rather than running it :param request_class: Request class to use :param access_log: disable/enable access log :param websocket_max_size: enforces the maximum size for @@ -771,7 +845,14 @@ def serve( ) if run_async: - return server_coroutine + return AsyncioServer( + loop, + server_coroutine, + connections, + after_start, + before_stop, + after_stop, + ) trigger_events(before_start, loop) diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 14798926..e66db66b 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -1,3 +1,4 @@ +import asyncio import signal import pytest @@ -89,3 +90,52 @@ async def test_trigger_before_events_create_server(app): assert hasattr(app, "db") assert isinstance(app.db, MySanicDb) + +def test_create_server_trigger_events(app): + """Test if create_server can trigger server events""" + + flag1 = False + flag2 = False + flag3 = False + + async def stop(app, loop): + nonlocal flag1 + flag1 = True + await asyncio.sleep(0.1) + app.stop() + + async def before_stop(app, loop): + nonlocal flag2 + flag2 = True + + async def after_stop(app, loop): + nonlocal flag3 + flag3 = True + + app.listener("after_server_start")(stop) + app.listener("before_server_stop")(before_stop) + app.listener("after_server_stop")(after_stop) + + loop = asyncio.get_event_loop() + serv_coro = app.create_server(return_asyncio_server=True) + serv_task = asyncio.ensure_future(serv_coro, loop=loop) + server = loop.run_until_complete(serv_task) + server.after_start() + try: + loop.run_forever() + except KeyboardInterrupt as e: + loop.stop() + finally: + # Run the on_stop function if provided + server.before_stop() + + # Wait for server to close + close_task = server.close() + loop.run_until_complete(close_task) + + # Complete all tasks on the loop + signal.stopped = True + for connection in server.connections: + connection.close_if_idle() + server.after_stop() + assert flag1 and flag2 and flag3 From 927c0e082e0fe22e33c00395dfcfc7b6fdf0b7dd Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Thu, 19 Sep 2019 03:22:24 +1000 Subject: [PATCH 04/19] Fix tests for multiprocessing pickle app and pickle blueprint (#1680) The old tests were not quite checking for the right thing. Fixing the test does not change Sanic code, expose any bugs, or fix any bugs. --- tests/test_multiprocessing.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 763db085..3cd60e56 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -5,6 +5,7 @@ import signal import pytest +from sanic import Blueprint from sanic.response import text from sanic.testing import HOST, PORT @@ -37,8 +38,6 @@ def test_multiprocessing(app): reason="SIGALRM is not implemented for this platform", ) def test_multiprocessing_with_blueprint(app): - from sanic import Blueprint - # Selects a number at random so we can spot check num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1)) process_list = set() @@ -64,27 +63,27 @@ def handler(request): return text("Hello") -# Muliprocessing on Windows requires app to be able to be pickled +# Multiprocessing on Windows requires app to be able to be pickled @pytest.mark.parametrize("protocol", [3, 4]) def test_pickle_app(app, protocol): app.route("/")(handler) p_app = pickle.dumps(app, protocol=protocol) + del app up_p_app = pickle.loads(p_app) assert up_p_app - request, response = app.test_client.get("/") + request, response = up_p_app.test_client.get("/") assert response.text == "Hello" @pytest.mark.parametrize("protocol", [3, 4]) def test_pickle_app_with_bp(app, protocol): - from sanic import Blueprint - bp = Blueprint("test_text") bp.route("/")(handler) app.blueprint(bp) p_app = pickle.dumps(app, protocol=protocol) + del app up_p_app = pickle.loads(p_app) assert up_p_app - request, response = app.test_client.get("/") - assert app.is_request_stream is False + request, response = up_p_app.test_client.get("/") + assert up_p_app.is_request_stream is False assert response.text == "Hello" From 6fc33812293845283c2080ccc3963dd21d4a01ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Dantas?= Date: Sun, 22 Sep 2019 17:55:36 -0300 Subject: [PATCH 05/19] Add a type checking pipeline (#1682) * Integrate with mypy --- .travis.yml | 6 ++++++ sanic/__main__.py | 6 +++++- sanic/app.py | 4 ++-- sanic/asgi.py | 34 +++++++++++++++++++++++++++++----- sanic/blueprint_group.py | 4 ++-- sanic/compat.py | 2 +- sanic/headers.py | 21 +++++++++++++-------- sanic/request.py | 6 +++--- sanic/response.py | 2 +- sanic/server.py | 6 +++--- sanic/static.py | 2 +- sanic/testing.py | 22 +++++++++++----------- sanic/views.py | 4 +++- sanic/websocket.py | 34 ++++++++++++++++++++++++---------- sanic/worker.py | 8 ++++---- tox.ini | 7 +++++++ 16 files changed, 115 insertions(+), 53 deletions(-) diff --git a/.travis.yml b/.travis.yml index d304c701..4553e325 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,12 @@ matrix: dist: xenial sudo: true name: "Python 3.7 without Extensions" + - env: TOX_ENV=type-checking + python: 3.6 + name: "Python 3.6 Type checks" + - env: TOX_ENV=type-checking + python: 3.7 + name: "Python 3.7 Type checks" - env: TOX_ENV=lint python: 3.6 name: "Python 3.6 Linter checks" diff --git a/sanic/__main__.py b/sanic/__main__.py index 73de3265..11320305 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -1,5 +1,6 @@ from argparse import ArgumentParser from importlib import import_module +from typing import Any, Dict, Optional from sanic.app import Sanic from sanic.log import logger @@ -35,7 +36,10 @@ if __name__ == "__main__": ) ) if args.cert is not None or args.key is not None: - ssl = {"cert": args.cert, "key": args.key} + ssl = { + "cert": args.cert, + "key": args.key, + } # type: Optional[Dict[str, Any]] else: ssl = None diff --git a/sanic/app.py b/sanic/app.py index 92e1a8dd..5cf06c07 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -11,7 +11,7 @@ from inspect import getmodulename, isawaitable, signature, stack from socket import socket from ssl import Purpose, SSLContext, create_default_context from traceback import format_exc -from typing import Any, Optional, Type, Union +from typing import Any, Dict, Optional, Type, Union from urllib.parse import urlencode, urlunparse from sanic import reloader_helpers @@ -768,7 +768,7 @@ class Sanic: URLBuildError """ # find the route by the supplied view name - kw = {} + kw: Dict[str, str] = {} # special static files url_for if view_name == "static": kw.update(name=kwargs.pop("name", "static")) diff --git a/sanic/asgi.py b/sanic/asgi.py index 8f7e5770..f9e49005 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -2,9 +2,23 @@ import asyncio import warnings from inspect import isawaitable -from typing import Any, Awaitable, Callable, MutableMapping, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + List, + MutableMapping, + Optional, + Tuple, + Union, +) from urllib.parse import quote +from requests_async import ASGISession # type: ignore + +import sanic.app # noqa + from sanic.compat import Header from sanic.exceptions import InvalidUsage, ServerError from sanic.log import logger @@ -54,6 +68,8 @@ class MockProtocol: class MockTransport: + _protocol: Optional[MockProtocol] + def __init__( self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend ) -> None: @@ -68,11 +84,12 @@ class MockTransport: self._protocol = MockProtocol(self, self.loop) return self._protocol - def get_extra_info(self, info: str) -> Union[str, bool]: + def get_extra_info(self, info: str) -> Union[str, bool, None]: if info == "peername": return self.scope.get("server") elif info == "sslcontext": return self.scope.get("scheme") in ["https", "wss"] + return None def get_websocket_connection(self) -> WebSocketConnection: try: @@ -172,6 +189,13 @@ class Lifespan: class ASGIApp: + sanic_app: Union[ASGISession, "sanic.app.Sanic"] + request: Request + transport: MockTransport + do_stream: bool + lifespan: Lifespan + ws: Optional[WebSocketConnection] + def __init__(self) -> None: self.ws = None @@ -182,8 +206,8 @@ class ASGIApp: instance = cls() instance.sanic_app = sanic_app instance.transport = MockTransport(scope, receive, send) - instance.transport.add_task = sanic_app.loop.create_task instance.transport.loop = sanic_app.loop + setattr(instance.transport, "add_task", sanic_app.loop.create_task) headers = Header( [ @@ -286,8 +310,8 @@ class ASGIApp: """ Write the response. """ - headers = [] - cookies = {} + headers: List[Tuple[bytes, bytes]] = [] + cookies: Dict[str, str] = {} try: cookies = { v.key: v diff --git a/sanic/blueprint_group.py b/sanic/blueprint_group.py index 3850cd20..e6e0ebbb 100644 --- a/sanic/blueprint_group.py +++ b/sanic/blueprint_group.py @@ -56,7 +56,7 @@ class BlueprintGroup(MutableSequence): """ return self._blueprints[item] - def __setitem__(self, index: int, item: object) -> None: + def __setitem__(self, index, item) -> None: """ Abstract method implemented to turn the `BlueprintGroup` class into a list like object to support all the existing behavior. @@ -69,7 +69,7 @@ class BlueprintGroup(MutableSequence): """ self._blueprints[index] = item - def __delitem__(self, index: int) -> None: + def __delitem__(self, index) -> None: """ Abstract method implemented to turn the `BlueprintGroup` class into a list like object to support all the existing behavior. diff --git a/sanic/compat.py b/sanic/compat.py index 6ca7e344..f5475695 100644 --- a/sanic/compat.py +++ b/sanic/compat.py @@ -1,4 +1,4 @@ -from multidict import CIMultiDict +from multidict import CIMultiDict # type: ignore class Header(CIMultiDict): diff --git a/sanic/headers.py b/sanic/headers.py index 6c9fa221..142ab27b 100644 --- a/sanic/headers.py +++ b/sanic/headers.py @@ -1,10 +1,10 @@ import re -from typing import Dict, Iterable, Optional, Tuple +from typing import Dict, Iterable, List, Optional, Tuple, Union from urllib.parse import unquote -Options = Dict[str, str] # key=value fields in various headers +Options = Dict[str, Union[int, str]] # key=value fields in various headers OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"' @@ -35,7 +35,7 @@ def parse_content_header(value: str) -> Tuple[str, Options]: value = _firefox_quote_escape.sub("%22", value) pos = value.find(";") if pos == -1: - options = {} + options: Dict[str, Union[int, str]] = {} else: options = { m.group(1).lower(): m.group(2) or m.group(3).replace("%22", '"') @@ -67,7 +67,7 @@ def parse_forwarded(headers, config) -> Optional[Options]: return None # Loop over = elements from right to left sep = pos = None - options = [] + options: List[Tuple[str, str]] = [] found = False for m in _rparam.finditer(header[::-1]): # Start of new element? (on parser skips and non-semicolon right sep) @@ -101,8 +101,13 @@ def parse_xforwarded(headers, config) -> Optional[Options]: try: # Combine, split and filter multiple headers' entries forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER) - proxies = (p.strip() for h in forwarded_for for p in h.split(",")) - proxies = [p for p in proxies if p] + proxies = [ + p + for p in ( + p.strip() for h in forwarded_for for p in h.split(",") + ) + if p + ] addr = proxies[-proxies_count] except (KeyError, IndexError): pass @@ -126,7 +131,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]: def fwd_normalize(fwd: OptionsIterable) -> Options: """Normalize and convert values extracted from forwarded headers.""" - ret = {} + ret: Dict[str, Union[int, str]] = {} for key, val in fwd: if val is not None: try: @@ -164,4 +169,4 @@ def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]: if not m: return None, None host, port = m.groups() - return host.lower(), port and int(port) + return host.lower(), int(port) if port is not None else None diff --git a/sanic/request.py b/sanic/request.py index 734ad0a3..690213a3 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -6,7 +6,7 @@ from collections import defaultdict, namedtuple from http.cookies import SimpleCookie from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse -from httptools import parse_url +from httptools import parse_url # type: ignore from sanic.exceptions import InvalidUsage from sanic.headers import ( @@ -19,9 +19,9 @@ from sanic.log import error_logger, logger try: - from ujson import loads as json_loads + from ujson import loads as json_loads # type: ignore except ImportError: - from json import loads as json_loads + from json import loads as json_loads # type: ignore DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" EXPECT_HEADER = "EXPECT" diff --git a/sanic/response.py b/sanic/response.py index e339819f..91fb25f4 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -3,7 +3,7 @@ from mimetypes import guess_type from os import path from urllib.parse import quote_plus -from aiofiles import open as open_async +from aiofiles import open as open_async # type: ignore from sanic.compat import Header from sanic.cookies import CookieJar diff --git a/sanic/server.py b/sanic/server.py index f18a27d7..fa38e435 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -10,8 +10,8 @@ from signal import signal as signal_func from socket import SO_REUSEADDR, SOL_SOCKET, socket from time import time -from httptools import HttpRequestParser -from httptools.parser.errors import HttpParserError +from httptools import HttpRequestParser # type: ignore +from httptools.parser.errors import HttpParserError # type: ignore from sanic.compat import Header from sanic.exceptions import ( @@ -28,7 +28,7 @@ from sanic.response import HTTPResponse try: - import uvloop + import uvloop # type: ignore if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) diff --git a/sanic/static.py b/sanic/static.py index 104a820b..cc020bc7 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -4,7 +4,7 @@ from re import sub from time import gmtime, strftime from urllib.parse import unquote -from aiofiles.os import stat +from aiofiles.os import stat # type: ignore from sanic.exceptions import ( ContentRangeError, diff --git a/sanic/testing.py b/sanic/testing.py index 06d75fc1..f4925b7f 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -6,9 +6,9 @@ from json import JSONDecodeError from socket import socket from urllib.parse import unquote, urlsplit -import httpcore -import requests_async as requests -import websockets +import httpcore # type: ignore +import requests_async as requests # type: ignore +import websockets # type: ignore from sanic.asgi import ASGIApp from sanic.exceptions import MethodNotSupported @@ -288,6 +288,14 @@ class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa request_complete = True return {"type": "http.request", "body": body_bytes} + request_complete = False + response_started = False + response_complete = False + raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any] + template = None + context = None + return_value = None + async def send(message) -> None: nonlocal raw_kwargs, response_started, response_complete, template, context # noqa @@ -316,14 +324,6 @@ class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa template = message["template"] context = message["context"] - request_complete = False - response_started = False - response_complete = False - raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any] - template = None - context = None - return_value = None - try: return_value = await self.app(scope, receive, send) except BaseException as exc: diff --git a/sanic/views.py b/sanic/views.py index 3bfe3850..c4574b8d 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -1,3 +1,5 @@ +from typing import Any, Callable, List + from sanic.constants import HTTP_METHODS from sanic.exceptions import InvalidUsage @@ -37,7 +39,7 @@ class HTTPMethodView: To add any decorator you could set it into decorators variable """ - decorators = [] + decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = [] def dispatch_request(self, request, *args, **kwargs): handler = getattr(self, request.method.lower(), None) diff --git a/sanic/websocket.py b/sanic/websocket.py index f87188e4..60092836 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -1,13 +1,27 @@ -from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + MutableMapping, + Optional, + Union, +) -from httptools import HttpParserUpgrade -from websockets import ConnectionClosed # noqa -from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake +from httptools import HttpParserUpgrade # type: ignore +from websockets import ( # type: ignore + ConnectionClosed, + InvalidHandshake, + WebSocketCommonProtocol, + handshake, +) from sanic.exceptions import InvalidUsage from sanic.server import HttpProtocol +__all__ = ["ConnectionClosed", "WebSocketProtocol", "WebSocketConnection"] + ASIMessage = MutableMapping[str, Any] @@ -125,14 +139,12 @@ class WebSocketConnection: self._receive = receive async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: - message = {"type": "websocket.send"} + message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"} - try: - data.decode() - except AttributeError: - message.update({"text": str(data)}) - else: + if isinstance(data, bytes): message.update({"bytes": data}) + else: + message.update({"text": str(data)}) await self._send(message) @@ -144,6 +156,8 @@ class WebSocketConnection: elif message["type"] == "websocket.disconnect": pass + return None + receive = recv async def accept(self) -> None: diff --git a/sanic/worker.py b/sanic/worker.py index ef8b2128..777f12cf 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -5,19 +5,19 @@ import signal import sys import traceback -import gunicorn.workers.base as base +import gunicorn.workers.base as base # type: ignore from sanic.server import HttpProtocol, Signal, serve, trigger_events from sanic.websocket import WebSocketProtocol try: - import ssl + import ssl # type: ignore except ImportError: - ssl = None + ssl = None # type: ignore try: - import uvloop + import uvloop # type: ignore asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except ImportError: diff --git a/tox.ini b/tox.ini index f8138edd..9eca7f58 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,13 @@ commands = black --config ./.black.toml --check --verbose sanic/ isort --check-only --recursive sanic +[testenv:type-checking] +deps = + mypy + +commands = + mypy sanic + [testenv:check] deps = docutils From c54a8b10bb848833defe366400079db1f09fa037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=2E=20K=C3=A4rkk=C3=A4inen?= <98187+Tronic@users.noreply.github.com> Date: Fri, 27 Sep 2019 00:11:31 +0300 Subject: [PATCH 06/19] Implement dict-like API on request objects for custom data. (#1666) * Implement dict-like API on request objects for custom data. * Updated docs about custom_context. --- docs/sanic/middleware.md | 33 ++++++++++++++++------ sanic/request.py | 33 ++++++++++++++++++---- tests/test_request_data.py | 58 +++++++++++++++++++++++++++++++++++--- tests/test_requests.py | 3 -- 4 files changed, 106 insertions(+), 21 deletions(-) diff --git a/docs/sanic/middleware.md b/docs/sanic/middleware.md index 820e3d85..f4c634e2 100644 --- a/docs/sanic/middleware.md +++ b/docs/sanic/middleware.md @@ -39,8 +39,8 @@ app = Sanic(__name__) @app.middleware('request') async def add_key(request): - # Add a key to request object like dict object - request['foo'] = 'bar' + # Arbitrary data may be stored in request context: + request.ctx.foo = 'bar' @app.middleware('response') @@ -53,16 +53,21 @@ async def prevent_xss(request, response): response.headers["x-xss-protection"] = "1; mode=block" +@app.get("/") +async def index(request): + return sanic.response.text(request.ctx.foo) + + app.run(host="0.0.0.0", port=8000) ``` -The above code will apply the three middleware in order. The first middleware -**add_key** will add a new key `foo` into `request` object. This worked because -`request` object can be manipulated like `dict` object. Then, the second middleware -**custom_banner** will change the HTTP response header *Server* to -*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP -header for preventing Cross-Site-Scripting (XSS) attacks. These two functions -are invoked *after* a user function returns a response. +The three middlewares are executed in order: + +1. The first request middleware **add_key** adds a new key `foo` into request context. +2. Request is routed to handler **index**, which gets the key from context and returns a text response. +3. The first response middleware **custom_banner** changes the HTTP response header *Server* to +say *Fake-Server* +4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks. ## Responding early @@ -81,6 +86,16 @@ async def halt_response(request, response): return text('I halted the response') ``` +## Custom context + +Arbitrary data may be stored in `request.ctx`. A typical use case +would be to store the user object acquired from database in an authentication +middleware. Keys added are accessible to all later middleware as well as +the handler over the duration of the request. + +Custom context is reserved for applications and extensions. Sanic itself makes +no use of it. + ## Listeners If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners: diff --git a/sanic/request.py b/sanic/request.py index 690213a3..972f7dbf 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -4,6 +4,7 @@ import warnings from collections import defaultdict, namedtuple from http.cookies import SimpleCookie +from types import SimpleNamespace from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse from httptools import parse_url # type: ignore @@ -62,7 +63,7 @@ class StreamBuffer: return self._queue.full() -class Request(dict): +class Request: """Properties of an HTTP request such as URL, headers, etc.""" __slots__ = ( @@ -75,6 +76,7 @@ class Request(dict): "_socket", "app", "body", + "ctx", "endpoint", "headers", "method", @@ -104,6 +106,7 @@ class Request(dict): # Init but do not inhale self.body_init() + self.ctx = SimpleNamespace() self.parsed_forwarded = None self.parsed_json = None self.parsed_form = None @@ -120,10 +123,30 @@ class Request(dict): self.__class__.__name__, self.method, self.path ) - def __bool__(self): - if self.transport: - return True - return False + def get(self, key, default=None): + """.. deprecated:: 19.9 + Custom context is now stored in `request.custom_context.yourkey`""" + return self.ctx.__dict__.get(key, default) + + def __contains__(self, key): + """.. deprecated:: 19.9 + Custom context is now stored in `request.custom_context.yourkey`""" + return key in self.ctx.__dict__ + + def __getitem__(self, key): + """.. deprecated:: 19.9 + Custom context is now stored in `request.custom_context.yourkey`""" + return self.ctx.__dict__[key] + + def __delitem__(self, key): + """.. deprecated:: 19.9 + Custom context is now stored in `request.custom_context.yourkey`""" + del self.ctx.__dict__[key] + + def __setitem__(self, key, value): + """.. deprecated:: 19.9 + Custom context is now stored in `request.custom_context.yourkey`""" + setattr(self.ctx, key, value) def body_init(self): self.body = [] diff --git a/tests/test_request_data.py b/tests/test_request_data.py index 061653bd..465ff53a 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -8,22 +8,72 @@ try: except ImportError: from json import loads - -def test_storage(app): +def test_custom_context(app): @app.middleware("request") def store(request): + request.ctx.user = "sanic" + request.ctx.session = None + + @app.route("/") + def handler(request): + # Accessing non-existant key should fail with AttributeError + try: + invalid = request.ctx.missing + except AttributeError as e: + invalid = str(e) + return json({ + "user": request.ctx.user, + "session": request.ctx.session, + "has_user": hasattr(request.ctx, "user"), + "has_session": hasattr(request.ctx, "session"), + "has_missing": hasattr(request.ctx, "missing"), + "invalid": invalid + }) + + request, response = app.test_client.get("/") + assert response.json == { + "user": "sanic", + "session": None, + "has_user": True, + "has_session": True, + "has_missing": False, + "invalid": "'types.SimpleNamespace' object has no attribute 'missing'", + } + + +# Remove this once the deprecated API is abolished. +def test_custom_context_old(app): + @app.middleware("request") + def store(request): + try: + request["foo"] + except KeyError: + pass request["user"] = "sanic" - request["sidekick"] = "tails" + sidekick = request.get("sidekick", "tails") # Item missing -> default + request["sidekick"] = sidekick + request["bar"] = request["sidekick"] del request["sidekick"] @app.route("/") def handler(request): return json( - {"user": request.get("user"), "sidekick": request.get("sidekick")} + { + "user": request.get("user"), + "sidekick": request.get("sidekick"), + "has_bar": "bar" in request, + "has_sidekick": "sidekick" in request, + } ) request, response = app.test_client.get("/") + assert response.json == { + "user": "sanic", + "sidekick": None, + "has_bar": True, + "has_sidekick": False, + } response_json = loads(response.text) assert response_json["user"] == "sanic" assert response_json.get("sidekick") is None diff --git a/tests/test_requests.py b/tests/test_requests.py index 522d806e..b6f91feb 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1499,9 +1499,6 @@ def test_request_bool(app): request, response = app.test_client.get("/") assert bool(request) - request.transport = False - assert not bool(request) - def test_request_parsing_form_failed(app, caplog): @app.route("/", methods=["POST"]) From 134c414fe57c94cdcf0dbd6107fdde1e5f7ac48e Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 2 Oct 2019 07:03:10 +0100 Subject: [PATCH 07/19] Support websockets 8.x as well as 7.x (#1687) Sanic currently requires websockets 7.x, but it's straightforward to also support the more recent 8.x. --- sanic/websocket.py | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/websocket.py b/sanic/websocket.py index 60092836..a2c7b81d 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -119,6 +119,9 @@ class WebSocketProtocol(HttpProtocol): read_limit=self.websocket_read_limit, write_limit=self.websocket_write_limit, ) + # Following two lines are required for websockets 8.x + self.websocket.is_client = False + self.websocket.side = "server" self.websocket.subprotocol = subprotocol self.websocket.connection_made(request.transport) self.websocket.connection_open() diff --git a/setup.py b/setup.py index 9d278fbf..5c60d087 100644 --- a/setup.py +++ b/setup.py @@ -80,7 +80,7 @@ requirements = [ uvloop, ujson, "aiofiles>=0.3.0", - "websockets>=7.0,<8.0", + "websockets>=7.0,<9.0", "multidict>=4.0,<5.0", "requests-async==0.5.0", ] From 01be69193666647aee6709fb2bc3ef8c7f005d78 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 7 Oct 2019 11:41:44 -0700 Subject: [PATCH 08/19] misc: bump up pytest version for fixing ci build --- .gitignore | 1 + setup.py | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 91cac370..6d4a42fa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ coverage settings.py .idea/* .cache/* +.mypy_cache/ .python-version docs/_build/ docs/_api/ diff --git a/setup.py b/setup.py index 5c60d087..7dc06f22 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ requirements = [ ] tests_require = [ - "pytest==4.1.0", + "pytest==5.2.1", "multidict>=4.0,<5.0", "gunicorn", "pytest-cov", diff --git a/tox.ini b/tox.ini index 9eca7f58..a1a99268 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ setenv = {py36,py37}-no-ext: SANIC_NO_UVLOOP=1 deps = coverage - pytest==4.1.0 + pytest==5.2.1 pytest-cov pytest-sanic pytest-sugar From 0df37fa653be6bc35553a734f0b9d4c20c09e1b2 Mon Sep 17 00:00:00 2001 From: Lagicrus Date: Wed, 9 Oct 2019 00:28:09 +0100 Subject: [PATCH 09/19] Update websocket.rst (#1694) --- docs/sanic/websocket.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sanic/websocket.rst b/docs/sanic/websocket.rst index 97dbae90..88a8e680 100644 --- a/docs/sanic/websocket.rst +++ b/docs/sanic/websocket.rst @@ -1,7 +1,10 @@ WebSocket ========= -Sanic provides an easy to use abstraction on top of `websockets`. To setup a WebSocket: +Sanic provides an easy to use abstraction on top of `websockets`. +Sanic Supports websocket versions 7 and 8. + +To setup a WebSocket: .. code:: python From 4f9739ed2ccc9fa3b1d406d96b7dd02f36f8ced4 Mon Sep 17 00:00:00 2001 From: Lagicrus Date: Wed, 9 Oct 2019 00:29:03 +0100 Subject: [PATCH 10/19] Update helpers.py (#1693) --- sanic/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/helpers.py b/sanic/helpers.py index 1b30e1ad..0ee80977 100644 --- a/sanic/helpers.py +++ b/sanic/helpers.py @@ -8,6 +8,7 @@ STATUS_CODES = { 100: b"Continue", 101: b"Switching Protocols", 102: b"Processing", + 103: b"Early Hints", 200: b"OK", 201: b"Created", 202: b"Accepted", From be0d539746f30b2b9e525152282d9946a8571190 Mon Sep 17 00:00:00 2001 From: 7 Date: Sat, 12 Oct 2019 07:54:47 -0700 Subject: [PATCH 11/19] 19.9.0 release (#1699) --- sanic/__version__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/__version__.py b/sanic/__version__.py index df1e23a8..08f1a61d 100644 --- a/sanic/__version__.py +++ b/sanic/__version__.py @@ -1 +1 @@ -__version__ = "19.6.3" +__version__ = "19.9.0" diff --git a/setup.cfg b/setup.cfg index f39f08c2..d0efa01a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ multi_line_output = 3 not_skip = __init__.py [version] -current_version = 19.6.3 +current_version = 19.9.0 files = sanic/__version__.py current_version_pattern = __version__ = "{current_version}" new_version_pattern = __version__ = "{new_version}" From fcdc9c83c51a4a08a1234c77542a6501485a75f8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 15 Oct 2019 01:17:05 -0300 Subject: [PATCH 12/19] Add 'python_requires' key to setup.py (#1701) This key is important so that `pip` doesn't try to install `sanic` in unsupported Python versions: https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 7dc06f22..bd48d994 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ setup_kwargs = { "long_description": long_description, "packages": ["sanic"], "platforms": "any", + "python_requires": ">=3.6", "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Web Environment", From e506c89304948bba593e8603ecace1c495b06fd5 Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Wed, 23 Oct 2019 21:42:20 +0530 Subject: [PATCH 13/19] deprecate None value support for app name (#1705) * :heavy_minus_sign: deprecate None value support for app name * :rotating_light: cleanup linter issues across the codebase --- sanic/app.py | 7 +++ tests/test_headers.py | 26 +++++---- tests/test_request_data.py | 19 ++++--- tests/test_requests.py | 105 ++++++++++++++++++++++-------------- tests/test_server_events.py | 1 + 5 files changed, 101 insertions(+), 57 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 5cf06c07..016ab73d 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -46,6 +46,13 @@ class Sanic: # Get name from previous stack frame 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, + ) frame_records = stack()[1] name = getmodulename(frame_records[1]) diff --git a/tests/test_headers.py b/tests/test_headers.py index e228f386..ad373ace 100644 --- a/tests/test_headers.py +++ b/tests/test_headers.py @@ -8,27 +8,33 @@ from sanic import headers [ ("text/plain", ("text/plain", {})), ("text/vnd.just.made.this.up ; ", ("text/vnd.just.made.this.up", {})), - ("text/plain;charset=us-ascii", ("text/plain", {"charset": "us-ascii"})), - ('text/plain ; charset="us-ascii"', ("text/plain", {"charset": "us-ascii"})), + ( + "text/plain;charset=us-ascii", + ("text/plain", {"charset": "us-ascii"}), + ), + ( + 'text/plain ; charset="us-ascii"', + ("text/plain", {"charset": "us-ascii"}), + ), ( 'text/plain ; charset="us-ascii"; another=opt', - ("text/plain", {"charset": "us-ascii", "another": "opt"}) + ("text/plain", {"charset": "us-ascii", "another": "opt"}), ), ( 'attachment; filename="silly.txt"', - ("attachment", {"filename": "silly.txt"}) + ("attachment", {"filename": "silly.txt"}), ), ( 'attachment; filename="strange;name"', - ("attachment", {"filename": "strange;name"}) + ("attachment", {"filename": "strange;name"}), ), ( 'attachment; filename="strange;name";size=123;', - ("attachment", {"filename": "strange;name", "size": "123"}) + ("attachment", {"filename": "strange;name", "size": "123"}), ), ( 'form-data; name="files"; filename="fo\\"o;bar\\"', - ('form-data', {'name': 'files', 'filename': 'fo"o;bar\\'}) + ("form-data", {"name": "files", "filename": 'fo"o;bar\\'}) # cgi.parse_header: # ('form-data', {'name': 'files', 'filename': 'fo"o;bar\\'}) # werkzeug.parse_options_header: @@ -39,7 +45,7 @@ from sanic import headers # Chrome: # Content-Disposition: form-data; name="foo%22;bar\"; filename="πŸ˜€" 'form-data; name="foo%22;bar\\"; filename="πŸ˜€"', - ('form-data', {'name': 'foo";bar\\', 'filename': 'πŸ˜€'}) + ("form-data", {"name": 'foo";bar\\', "filename": "πŸ˜€"}) # cgi: ('form-data', {'name': 'foo%22;bar"; filename="πŸ˜€'}) # werkzeug: ('form-data', {'name': 'foo%22;bar"; filename='}) ), @@ -47,11 +53,11 @@ from sanic import headers # Firefox: # Content-Disposition: form-data; name="foo\";bar\"; filename="πŸ˜€" 'form-data; name="foo\\";bar\\"; filename="πŸ˜€"', - ('form-data', {'name': 'foo";bar\\', 'filename': 'πŸ˜€'}) + ("form-data", {"name": 'foo";bar\\', "filename": "πŸ˜€"}) # cgi: ('form-data', {'name': 'foo";bar"; filename="πŸ˜€'}) # werkzeug: ('form-data', {'name': 'foo";bar"; filename='}) ), - ] + ], ) def test_parse_headers(input, expected): assert headers.parse_content_header(input) == expected diff --git a/tests/test_request_data.py b/tests/test_request_data.py index 465ff53a..684561de 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -8,6 +8,7 @@ try: except ImportError: from json import loads + def test_custom_context(app): @app.middleware("request") def store(request): @@ -21,14 +22,16 @@ def test_custom_context(app): invalid = request.ctx.missing except AttributeError as e: invalid = str(e) - return json({ - "user": request.ctx.user, - "session": request.ctx.session, - "has_user": hasattr(request.ctx, "user"), - "has_session": hasattr(request.ctx, "session"), - "has_missing": hasattr(request.ctx, "missing"), - "invalid": invalid - }) + return json( + { + "user": request.ctx.user, + "session": request.ctx.session, + "has_user": hasattr(request.ctx, "user"), + "has_session": hasattr(request.ctx, "session"), + "has_missing": hasattr(request.ctx, "missing"), + "invalid": invalid, + } + ) request, response = app.test_client.get("/") assert response.json == { diff --git a/tests/test_requests.py b/tests/test_requests.py index b6f91feb..5d0062e4 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -413,15 +413,15 @@ def test_standard_forwarded(app): "Forwarded": ( 'for=1.1.1.1, for=injected;host="' ', for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret' - ',for=broken;;secret=b0rked' - ', for=127.0.0.3;scheme=http;port=1234' + ",for=broken;;secret=b0rked" + ", for=127.0.0.3;scheme=http;port=1234" ), "X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1", "X-Scheme": "ws", } request, response = app.test_client.get("/", headers=headers) - assert response.json == { "for": "127.0.0.2", "proto": "ws" } + assert response.json == {"for": "127.0.0.2", "proto": "ws"} assert request.remote_addr == "127.0.0.2" assert request.scheme == "ws" assert request.server_port == 80 @@ -433,7 +433,7 @@ def test_standard_forwarded(app): "proto": "https", "host": "me.tld", "path": "/app/", - "secret": "mySecret" + "secret": "mySecret", } assert request.remote_addr == "[::2]" assert request.server_name == "me.tld" @@ -443,7 +443,7 @@ def test_standard_forwarded(app): # Empty Forwarded header -> use X-headers headers["Forwarded"] = "" request, response = app.test_client.get("/", headers=headers) - assert response.json == { "for": "127.0.0.2", "proto": "ws" } + assert response.json == {"for": "127.0.0.2", "proto": "ws"} # Header present but not matching anything request, response = app.test_client.get("/", headers={"Forwarded": "."}) @@ -451,8 +451,8 @@ def test_standard_forwarded(app): # Forwarded header present but no matching secret -> use X-headers headers = { - "Forwarded": 'for=1.1.1.1;secret=x, for=127.0.0.1', - "X-Real-IP": "127.0.0.2" + "Forwarded": "for=1.1.1.1;secret=x, for=127.0.0.1", + "X-Real-IP": "127.0.0.2", } request, response = app.test_client.get("/", headers=headers) assert response.json == {"for": "127.0.0.2"} @@ -464,7 +464,7 @@ def test_standard_forwarded(app): assert response.json == { "for": "127.0.0.4", "port": 1234, - "secret": "mySecret" + "secret": "mySecret", } # Test escapes (modify this if you see anyone implementing quoted-pairs) @@ -472,29 +472,29 @@ def test_standard_forwarded(app): request, response = app.test_client.get("/", headers=headers) assert response.json == { "for": "test", - "quoted": '\\,x=x;y=\\', - "secret": "mySecret" + "quoted": "\\,x=x;y=\\", + "secret": "mySecret", } # Secret insulated by malformed field #1 - headers = {"Forwarded": 'for=test;secret=mySecret;b0rked;proto=wss;'} + headers = {"Forwarded": "for=test;secret=mySecret;b0rked;proto=wss;"} request, response = app.test_client.get("/", headers=headers) assert response.json == {"for": "test", "secret": "mySecret"} # Secret insulated by malformed field #2 - headers = {"Forwarded": 'for=test;b0rked;secret=mySecret;proto=wss'} + headers = {"Forwarded": "for=test;b0rked;secret=mySecret;proto=wss"} request, response = app.test_client.get("/", headers=headers) assert response.json == {"proto": "wss", "secret": "mySecret"} # Unexpected termination should not lose existing acceptable values - headers = {"Forwarded": 'b0rked;secret=mySecret;proto=wss'} + headers = {"Forwarded": "b0rked;secret=mySecret;proto=wss"} request, response = app.test_client.get("/", headers=headers) assert response.json == {"proto": "wss", "secret": "mySecret"} # Field normalization headers = { "Forwarded": 'PROTO=WSS;BY="CAFE::8000";FOR=unknown;PORT=X;HOST="A:2";' - 'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' + 'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' } request, response = app.test_client.get("/", headers=headers) assert response.json == { @@ -507,7 +507,7 @@ def test_standard_forwarded(app): # Using "by" field as secret app.config.FORWARDED_SECRET = "_proxySecret" - headers = {"Forwarded": 'for=1.2.3.4; by=_proxySecret'} + headers = {"Forwarded": "for=1.2.3.4; by=_proxySecret"} request, response = app.test_client.get("/", headers=headers) assert response.json == {"for": "1.2.3.4", "by": "_proxySecret"} @@ -525,15 +525,15 @@ async def test_standard_forwarded_asgi(app): "Forwarded": ( 'for=1.1.1.1, for=injected;host="' ', for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret' - ',for=broken;;secret=b0rked' - ', for=127.0.0.3;scheme=http;port=1234' + ",for=broken;;secret=b0rked" + ", for=127.0.0.3;scheme=http;port=1234" ), "X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1", "X-Scheme": "ws", } request, response = await app.asgi_client.get("/", headers=headers) - assert response.json() == { "for": "127.0.0.2", "proto": "ws" } + assert response.json() == {"for": "127.0.0.2", "proto": "ws"} assert request.remote_addr == "127.0.0.2" assert request.scheme == "ws" assert request.server_port == 80 @@ -545,7 +545,7 @@ async def test_standard_forwarded_asgi(app): "proto": "https", "host": "me.tld", "path": "/app/", - "secret": "mySecret" + "secret": "mySecret", } assert request.remote_addr == "[::2]" assert request.server_name == "me.tld" @@ -555,16 +555,18 @@ async def test_standard_forwarded_asgi(app): # Empty Forwarded header -> use X-headers headers["Forwarded"] = "" request, response = await app.asgi_client.get("/", headers=headers) - assert response.json() == { "for": "127.0.0.2", "proto": "ws" } + assert response.json() == {"for": "127.0.0.2", "proto": "ws"} # Header present but not matching anything - request, response = await app.asgi_client.get("/", headers={"Forwarded": "."}) + request, response = await app.asgi_client.get( + "/", headers={"Forwarded": "."} + ) assert response.json() == {} # Forwarded header present but no matching secret -> use X-headers headers = { - "Forwarded": 'for=1.1.1.1;secret=x, for=127.0.0.1', - "X-Real-IP": "127.0.0.2" + "Forwarded": "for=1.1.1.1;secret=x, for=127.0.0.1", + "X-Real-IP": "127.0.0.2", } request, response = await app.asgi_client.get("/", headers=headers) assert response.json() == {"for": "127.0.0.2"} @@ -576,7 +578,7 @@ async def test_standard_forwarded_asgi(app): assert response.json() == { "for": "127.0.0.4", "port": 1234, - "secret": "mySecret" + "secret": "mySecret", } # Test escapes (modify this if you see anyone implementing quoted-pairs) @@ -584,29 +586,29 @@ async def test_standard_forwarded_asgi(app): request, response = await app.asgi_client.get("/", headers=headers) assert response.json() == { "for": "test", - "quoted": '\\,x=x;y=\\', - "secret": "mySecret" + "quoted": "\\,x=x;y=\\", + "secret": "mySecret", } # Secret insulated by malformed field #1 - headers = {"Forwarded": 'for=test;secret=mySecret;b0rked;proto=wss;'} + headers = {"Forwarded": "for=test;secret=mySecret;b0rked;proto=wss;"} request, response = await app.asgi_client.get("/", headers=headers) assert response.json() == {"for": "test", "secret": "mySecret"} # Secret insulated by malformed field #2 - headers = {"Forwarded": 'for=test;b0rked;secret=mySecret;proto=wss'} + headers = {"Forwarded": "for=test;b0rked;secret=mySecret;proto=wss"} request, response = await app.asgi_client.get("/", headers=headers) assert response.json() == {"proto": "wss", "secret": "mySecret"} # Unexpected termination should not lose existing acceptable values - headers = {"Forwarded": 'b0rked;secret=mySecret;proto=wss'} + headers = {"Forwarded": "b0rked;secret=mySecret;proto=wss"} request, response = await app.asgi_client.get("/", headers=headers) assert response.json() == {"proto": "wss", "secret": "mySecret"} # Field normalization headers = { "Forwarded": 'PROTO=WSS;BY="CAFE::8000";FOR=unknown;PORT=X;HOST="A:2";' - 'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' + 'PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' } request, response = await app.asgi_client.get("/", headers=headers) assert response.json() == { @@ -619,7 +621,7 @@ async def test_standard_forwarded_asgi(app): # Using "by" field as secret app.config.FORWARDED_SECRET = "_proxySecret" - headers = {"Forwarded": 'for=1.2.3.4; by=_proxySecret'} + headers = {"Forwarded": "for=1.2.3.4; by=_proxySecret"} request, response = await app.asgi_client.get("/", headers=headers) assert response.json() == {"for": "1.2.3.4", "by": "_proxySecret"} @@ -813,11 +815,14 @@ def test_forwarded_scheme(app): assert request.scheme == "http" request, response = app.test_client.get( - "/", headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https"} + "/", + headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https"}, ) assert request.scheme == "https" - request, response = app.test_client.get("/", headers={"X-Forwarded-For": "127.1.2.3", "X-Scheme": "https"}) + request, response = app.test_client.get( + "/", headers={"X-Forwarded-For": "127.1.2.3", "X-Scheme": "https"} + ) assert request.scheme == "https" @@ -1872,7 +1877,7 @@ def test_request_server_name_in_host_header(app): request, response = app.test_client.get( "/", headers={"Host": "mal_formed"} ) - assert request.server_name == None # For now (later maybe 127.0.0.1) + assert request.server_name == None # For now (later maybe 127.0.0.1) def test_request_server_name_forwarded(app): @@ -1883,7 +1888,11 @@ def test_request_server_name_forwarded(app): app.config.PROXIES_COUNT = 1 request, response = app.test_client.get( "/", - headers={"Host": "my-server:5555", "X-Forwarded-For": "127.1.2.3", "X-Forwarded-Host": "your-server"}, + headers={ + "Host": "my-server:5555", + "X-Forwarded-For": "127.1.2.3", + "X-Forwarded-Host": "your-server", + }, ) assert request.server_name == "your-server" @@ -1925,7 +1934,12 @@ def test_request_server_port_forwarded(app): app.config.PROXIES_COUNT = 1 request, response = app.test_client.get( - "/", headers={"Host": "my-server:5555", "X-Forwarded-For": "127.1.2.3", "X-Forwarded-Port": "4444"} + "/", + headers={ + "Host": "my-server:5555", + "X-Forwarded-For": "127.1.2.3", + "X-Forwarded-Port": "4444", + }, ) assert request.server_port == 4444 @@ -1948,7 +1962,10 @@ def test_server_name_and_url_for(app): app.config.SERVER_NAME = "my-server" assert app.url_for("handler", _external=True) == "http://my-server/foo" request, response = app.test_client.get("/foo") - assert request.url_for("handler") == f"http://my-server:{app.test_client.port}/foo" + assert ( + request.url_for("handler") + == f"http://my-server:{app.test_client.port}/foo" + ) app.config.SERVER_NAME = "https://my-server/path" request, response = app.test_client.get("/foo") @@ -1969,7 +1986,12 @@ def test_url_for_with_forwarded_request(app): app.config.SERVER_NAME = "my-server" app.config.PROXIES_COUNT = 1 request, response = app.test_client.get( - "/", headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https", "X-Forwarded-Port": "6789"} + "/", + headers={ + "X-Forwarded-For": "127.1.2.3", + "X-Forwarded-Proto": "https", + "X-Forwarded-Port": "6789", + }, ) assert app.url_for("view_name") == "/another_view" assert ( @@ -1981,7 +2003,12 @@ def test_url_for_with_forwarded_request(app): ) request, response = app.test_client.get( - "/", headers={"X-Forwarded-For": "127.1.2.3", "X-Forwarded-Proto": "https", "X-Forwarded-Port": "443"} + "/", + headers={ + "X-Forwarded-For": "127.1.2.3", + "X-Forwarded-Proto": "https", + "X-Forwarded-Port": "443", + }, ) assert request.url_for("view_name") == "https://my-server/another_view" diff --git a/tests/test_server_events.py b/tests/test_server_events.py index e66db66b..1a74d960 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -91,6 +91,7 @@ async def test_trigger_before_events_create_server(app): assert hasattr(app, "db") assert isinstance(app.db, MySanicDb) + def test_create_server_trigger_events(app): """Test if create_server can trigger server events""" From e81a8ce07329e95d3d0899b1d774f21759c28e0e Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Fri, 1 Nov 2019 23:02:49 +0530 Subject: [PATCH 14/19] fix SERVER_NAME enforcement in url_for and request.args documentation (#1708) * :bug: fix SERVER_NAME enforcement in url_for fixes #1707 * :bulb: add additional documentation for using request.args fixes #1704 * :white_check_mark: add additional test to check url_for without SERVER_NAME * :pencil: add changelog for fixes --- changelogs/1704.doc.rst | 3 +++ changelogs/1707.bugfix.rst | 4 ++++ docs/sanic/request_data.md | 18 +++++++++++++++--- sanic/request.py | 7 +++++-- tests/test_requests.py | 16 ++++++++++++++++ 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 changelogs/1704.doc.rst create mode 100644 changelogs/1707.bugfix.rst diff --git a/changelogs/1704.doc.rst b/changelogs/1704.doc.rst new file mode 100644 index 00000000..873ce539 --- /dev/null +++ b/changelogs/1704.doc.rst @@ -0,0 +1,3 @@ +Fix documentation for `get` and `getlist` of the `request.args` + +Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior diff --git a/changelogs/1707.bugfix.rst b/changelogs/1707.bugfix.rst new file mode 100644 index 00000000..a98cd6b4 --- /dev/null +++ b/changelogs/1707.bugfix.rst @@ -0,0 +1,4 @@ +Fix `url_for` behavior with missing SERVER_NAME + +If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing +due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index 34f87bd5..eb8f12fa 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -204,9 +204,8 @@ The output will be: ## Accessing values using `get` and `getlist` -The request properties which return a dictionary actually return a subclass of -`dict` called `RequestParameters`. The key difference when using this object is -the distinction between the `get` and `getlist` methods. +The `request.args` returns a subclass of `dict` called `RequestParameters`. +The key difference when using this object is the distinction between the `get` and `getlist` methods. - `get(key, default=None)` operates as normal, except that when the value of the given key is a list, *only the first item is returned*. @@ -223,6 +222,19 @@ args.get('titles') # => 'Post 1' args.getlist('titles') # => ['Post 1', 'Post 2'] ``` +```python +from sanic import Sanic +from sanic.response import json + +app = Sanic(name="example") + +@app.route("/") +def get_handler(request): + return json({ + "p1": request.args.getlist("p1") + }) +``` + ## Accessing the handler name with the request.endpoint attribute The `request.endpoint` attribute holds the handler's name. For instance, the below diff --git a/sanic/request.py b/sanic/request.py index 972f7dbf..9712aeb3 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -519,8 +519,11 @@ class Request: :rtype: str """ # Full URL SERVER_NAME can only be handled in app.url_for - if "//" in self.app.config.SERVER_NAME: - return self.app.url_for(view_name, _external=True, **kwargs) + try: + if "//" in self.app.config.SERVER_NAME: + return self.app.url_for(view_name, _external=True, **kwargs) + except AttributeError: + pass scheme = self.scheme host = self.server_name diff --git a/tests/test_requests.py b/tests/test_requests.py index 5d0062e4..2f83513c 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2103,3 +2103,19 @@ async def test_endpoint_blueprint_asgi(): request, response = await app.asgi_client.get("/bp") assert request.endpoint == "named.my_blueprint.bp_root" + + +def test_url_for_without_server_name(app): + @app.route("/sample") + def sample(request): + return json({"url": request.url_for("url_for")}) + + @app.route("/url-for") + def url_for(request): + return text("url-for") + + request, response = app.test_client.get("/sample") + assert ( + response.json["url"] + == f"http://127.0.0.1:{app.test_client.port}/url-for" + ) From a4185a0ba7b157bf6dc450e49eff5fe825f6766d Mon Sep 17 00:00:00 2001 From: Lagicrus Date: Thu, 14 Nov 2019 20:11:38 +0000 Subject: [PATCH 15/19] Doc rework (#1698) * blueprints * class_based_views * config * decorators * deploying * exceptions * extensions * getting_started * middleware * request_data * response * routing * static_files * streaming * testing * versioning * Fix bug and links * spelling mistakes * Bug fixes and minor tweaks * Create 1691.doc.rst * Bug fixes and tweaks Co-Authored-By: Harsha Narayana --- CONTRIBUTING.rst | 2 +- changelogs/1691.doc.rst | 4 + docs/index.html | 468 ++++++++++++++++++ docs/sanic/blueprints.md | 286 ----------- docs/sanic/blueprints.rst | 301 +++++++++++ docs/sanic/class_based_views.md | 166 ------- docs/sanic/class_based_views.rst | 169 +++++++ docs/sanic/config.md | 211 -------- docs/sanic/config.rst | 242 +++++++++ docs/sanic/debug_mode.rst | 4 +- docs/sanic/decorators.md | 39 -- docs/sanic/decorators.rst | 40 ++ docs/sanic/{deploying.md => deploying.rst} | 198 ++++---- docs/sanic/exceptions.md | 88 ---- docs/sanic/exceptions.rst | 92 ++++ docs/sanic/extensions.md | 3 - docs/sanic/extensions.rst | 4 + docs/sanic/getting_started.md | 57 --- docs/sanic/getting_started.rst | 62 +++ docs/sanic/index.rst | 3 +- docs/sanic/{middleware.md => middleware.rst} | 162 +++--- .../{request_data.md => request_data.rst} | 214 ++++---- docs/sanic/response.md | 114 ----- docs/sanic/response.rst | 126 +++++ docs/sanic/routing.md | 429 ---------------- docs/sanic/routing.rst | 433 ++++++++++++++++ docs/sanic/static_files.md | 87 ---- docs/sanic/static_files.rst | 92 ++++ docs/sanic/streaming.md | 143 ------ docs/sanic/streaming.rst | 147 ++++++ docs/sanic/testing.md | 144 ------ docs/sanic/testing.rst | 145 ++++++ docs/sanic/versioning.md | 50 -- docs/sanic/versioning.rst | 54 ++ 34 files changed, 2679 insertions(+), 2100 deletions(-) create mode 100644 changelogs/1691.doc.rst create mode 100644 docs/index.html delete mode 100644 docs/sanic/blueprints.md create mode 100644 docs/sanic/blueprints.rst delete mode 100644 docs/sanic/class_based_views.md create mode 100644 docs/sanic/class_based_views.rst delete mode 100644 docs/sanic/config.md create mode 100644 docs/sanic/config.rst delete mode 100644 docs/sanic/decorators.md create mode 100644 docs/sanic/decorators.rst rename docs/sanic/{deploying.md => deploying.rst} (53%) delete mode 100644 docs/sanic/exceptions.md create mode 100644 docs/sanic/exceptions.rst delete mode 100644 docs/sanic/extensions.md create mode 100644 docs/sanic/extensions.rst delete mode 100644 docs/sanic/getting_started.md create mode 100644 docs/sanic/getting_started.rst rename docs/sanic/{middleware.md => middleware.rst} (50%) rename docs/sanic/{request_data.md => request_data.rst} (59%) delete mode 100644 docs/sanic/response.md create mode 100644 docs/sanic/response.rst delete mode 100644 docs/sanic/routing.md create mode 100644 docs/sanic/routing.rst delete mode 100644 docs/sanic/static_files.md create mode 100644 docs/sanic/static_files.rst delete mode 100644 docs/sanic/streaming.md create mode 100644 docs/sanic/streaming.rst delete mode 100644 docs/sanic/testing.md create mode 100644 docs/sanic/testing.rst delete mode 100644 docs/sanic/versioning.md create mode 100644 docs/sanic/versioning.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5d969477..b08a60db 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -68,7 +68,7 @@ run all unittests, perform lint and other checks. Run unittests ------------- -``tox`` environment -> ``[testenv]` +``tox`` environment -> ``[testenv]`` To execute only unittests, run ``tox`` with environment like so: diff --git a/changelogs/1691.doc.rst b/changelogs/1691.doc.rst new file mode 100644 index 00000000..e4a9e3de --- /dev/null +++ b/changelogs/1691.doc.rst @@ -0,0 +1,4 @@ +Move docs from RST to MD + +Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in +the future to update documentation. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..54ca5ba3 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,468 @@ + + + + + + +index.rst + + + +
+ + +
+

Sanic

+

Sanic is a Python 3.6+ web server and web framework that's written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy.

+

The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.

+

Sanic is developed on GitHub. Contributions are welcome!

+
+

Sanic aspires to be simple

+
+from sanic import Sanic
+from sanic.response import json
+
+app = Sanic()
+
+@app.route("/")
+async def test(request):
+    return json({"hello": "world"})
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=8000)
+
+
+

Note

+

Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru +December 2020. Official Python support for version 3.5 is set to expire in September 2020.

+
+
+
+
+

Guides

+
+

System Message: ERROR/3 (E:/OneDrive/GitHub/sanic/docs/index.rst, line 6)

+

Unknown directive type "toctree".

+
+.. toctree::
+   :maxdepth: 2
+
+   sanic/getting_started
+   sanic/config
+   sanic/logging
+   sanic/request_data
+   sanic/response
+   sanic/cookies
+   sanic/routing
+   sanic/blueprints
+   sanic/static_files
+   sanic/versioning
+   sanic/exceptions
+   sanic/middleware
+   sanic/websocket
+   sanic/decorators
+   sanic/streaming
+   sanic/class_based_views
+   sanic/custom_protocol
+   sanic/sockets
+   sanic/ssl
+   sanic/debug_mode
+   sanic/testing
+   sanic/deploying
+   sanic/extensions
+   sanic/examples
+   sanic/changelog
+   sanic/contributing
+   sanic/api_reference
+   sanic/asyncio_python37
+
+
+
+
+
+
+

Module Documentation

+
+

System Message: ERROR/3 (E:/OneDrive/GitHub/sanic/docs/index.rst, line 42)

+

Unknown directive type "toctree".

+
+.. toctree::
+
+
+
+
    +
  • :ref:`genindex`

    +
    +

    System Message: ERROR/3 (E:/OneDrive/GitHub/sanic/docs/index.rst, line 44); backlink

    +

    Unknown interpreted text role "ref".

    +
    +
  • +
  • :ref:`modindex`

    +
    +

    System Message: ERROR/3 (E:/OneDrive/GitHub/sanic/docs/index.rst, line 45); backlink

    +

    Unknown interpreted text role "ref".

    +
    +
  • +
  • :ref:`search`

    +
    +

    System Message: ERROR/3 (E:/OneDrive/GitHub/sanic/docs/index.rst, line 46); backlink

    +

    Unknown interpreted text role "ref".

    +
    +
  • +
+
+
+ + diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md deleted file mode 100644 index 6edbf1a8..00000000 --- a/docs/sanic/blueprints.md +++ /dev/null @@ -1,286 +0,0 @@ -# Blueprints - -Blueprints are objects that can be used for sub-routing within an application. -Instead of adding routes to the application instance, blueprints define similar -methods for adding routes, which are then registered with the application in a -flexible and pluggable manner. - -Blueprints are especially useful for larger applications, where your -application logic can be broken down into several groups or areas of -responsibility. - -## My First Blueprint - -The following shows a very simple blueprint that registers a handler-function at -the root `/` of your application. - -Suppose you save this file as `my_blueprint.py`, which can be imported into your -main application later. - -```python -from sanic.response import json -from sanic import Blueprint - -bp = Blueprint('my_blueprint') - -@bp.route('/') -async def bp_root(request): - return json({'my': 'blueprint'}) - -``` - -## Registering blueprints - -Blueprints must be registered with the application. - -```python -from sanic import Sanic -from my_blueprint import bp - -app = Sanic(__name__) -app.blueprint(bp) - -app.run(host='0.0.0.0', port=8000, debug=True) -``` - -This will add the blueprint to the application and register any routes defined -by that blueprint. In this example, the registered routes in the `app.router` -will look like: - -```python -[Route(handler=, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')] -``` - -## Blueprint groups and nesting - -Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example: - -``` -api/ -β”œβ”€β”€content/ -β”‚ β”œβ”€β”€authors.py -β”‚ β”œβ”€β”€static.py -β”‚ └──__init__.py -β”œβ”€β”€info.py -└──__init__.py -app.py -``` - -Initialization of this app's blueprint hierarchy could go as follows: - -```python -# api/content/authors.py -from sanic import Blueprint - -authors = Blueprint('content_authors', url_prefix='/authors') -``` -```python -# api/content/static.py -from sanic import Blueprint - -static = Blueprint('content_static', url_prefix='/static') -``` -```python -# api/content/__init__.py -from sanic import Blueprint - -from .static import static -from .authors import authors - -content = Blueprint.group(static, authors, url_prefix='/content') -``` -```python -# api/info.py -from sanic import Blueprint - -info = Blueprint('info', url_prefix='/info') -``` -```python -# api/__init__.py -from sanic import Blueprint - -from .content import content -from .info import info - -api = Blueprint.group(content, info, url_prefix='/api') -``` - -And registering these blueprints in `app.py` can now be done like so: - -```python -# app.py -from sanic import Sanic - -from .api import api - -app = Sanic(__name__) - -app.blueprint(api) -``` - -## Using Blueprints - -Blueprints have almost the same functionality as an application instance. - -### WebSocket routes - -WebSocket handlers can be registered on a blueprint using the `@bp.websocket` -decorator or `bp.add_websocket_route` method. - -### Blueprint Middleware - -Using blueprints allows you to also register middleware globally. - -```python -@bp.middleware -async def print_on_request(request): - print("I am a spy") - -@bp.middleware('request') -async def halt_request(request): - return text('I halted the request') - -@bp.middleware('response') -async def halt_response(request, response): - return text('I halted the response') -``` - -### Blueprint Group Middleware -Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the -current blueprint group under consideration. - -```python -bp1 = Blueprint('bp1', url_prefix='/bp1') -bp2 = Blueprint('bp2', url_prefix='/bp2') - -@bp1.middleware('request') -async def bp1_only_middleware(request): - print('applied on Blueprint : bp1 Only') - -@bp1.route('/') -async def bp1_route(request): - return text('bp1') - -@bp2.route('/') -async def bp2_route(request, param): - return text(param) - -group = Blueprint.group(bp1, bp2) - -@group.middleware('request') -async def group_middleware(request): - print('common middleware applied for both bp1 and bp2') - -# Register Blueprint group under the app -app.blueprint(group) -``` - -### Exceptions - -Exceptions can be applied exclusively to blueprints globally. - -```python -@bp.exception(NotFound) -def ignore_404s(request, exception): - return text("Yep, I totally found the page: {}".format(request.url)) -``` - -### Static files - -Static files can be served globally, under the blueprint prefix. - -```python - -# suppose bp.name == 'bp' - -bp.static('/web/path', '/folder/to/serve') -# also you can pass name parameter to it for url_for -bp.static('/web/path', '/folder/to/server', name='uploads') -app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt' - -``` - -## Start and stop - -Blueprints can run functions during the start and stop process of the server. -If running in multiprocessor mode (more than 1 worker), these are triggered -after the workers fork. - -Available events are: - -- `before_server_start`: Executed before the server begins to accept connections -- `after_server_start`: Executed after the server begins to accept connections -- `before_server_stop`: Executed before the server stops accepting connections -- `after_server_stop`: Executed after the server is stopped and all requests are complete - -```python -bp = Blueprint('my_blueprint') - -@bp.listener('before_server_start') -async def setup_connection(app, loop): - global database - database = mysql.connect(host='127.0.0.1'...) - -@bp.listener('after_server_stop') -async def close_connection(app, loop): - await database.close() -``` - -## Use-case: API versioning - -Blueprints can be very useful for API versioning, where one blueprint may point -at `/v1/`, and another pointing at `/v2/`. - -When a blueprint is initialised, it can take an optional `version` argument, -which will be prepended to all routes defined on the blueprint. This feature -can be used to implement our API versioning scheme. - -```python -# blueprints.py -from sanic.response import text -from sanic import Blueprint - -blueprint_v1 = Blueprint('v1', url_prefix='/api', version="v1") -blueprint_v2 = Blueprint('v2', url_prefix='/api', version="v2") - -@blueprint_v1.route('/') -async def api_v1_root(request): - return text('Welcome to version 1 of our documentation') - -@blueprint_v2.route('/') -async def api_v2_root(request): - return text('Welcome to version 2 of our documentation') -``` - -When we register our blueprints on the app, the routes `/v1/api` and `/v2/api` will now -point to the individual blueprints, which allows the creation of *sub-sites* -for each API version. - -```python -# main.py -from sanic import Sanic -from blueprints import blueprint_v1, blueprint_v2 - -app = Sanic(__name__) -app.blueprint(blueprint_v1) -app.blueprint(blueprint_v2) - -app.run(host='0.0.0.0', port=8000, debug=True) -``` - -## URL Building with `url_for` - -If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name -takes the format `.`. For example: - -```python -@blueprint_v1.route('/') -async def root(request): - url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5' - return redirect(url) - - -@blueprint_v1.route('/post/') -async def post_handler(request, post_id): - return text('Post {} in Blueprint V1'.format(post_id)) -``` diff --git a/docs/sanic/blueprints.rst b/docs/sanic/blueprints.rst new file mode 100644 index 00000000..710467f8 --- /dev/null +++ b/docs/sanic/blueprints.rst @@ -0,0 +1,301 @@ +Blueprints +========== + +Blueprints are objects that can be used for sub-routing within an application. +Instead of adding routes to the application instance, blueprints define similar +methods for adding routes, which are then registered with the application in a +flexible and pluggable manner. + +Blueprints are especially useful for larger applications, where your +application logic can be broken down into several groups or areas of +responsibility. + +My First Blueprint +------------------ + +The following shows a very simple blueprint that registers a handler-function at +the root `/` of your application. + +Suppose you save this file as `my_blueprint.py`, which can be imported into your +main application later. + +.. code-block:: python + + from sanic.response import json + from sanic import Blueprint + + bp = Blueprint('my_blueprint') + + @bp.route('/') + async def bp_root(request): + return json({'my': 'blueprint'}) + +Registering blueprints +---------------------- + +Blueprints must be registered with the application. + +.. code-block:: python + + from sanic import Sanic + from my_blueprint import bp + + app = Sanic(__name__) + app.blueprint(bp) + + app.run(host='0.0.0.0', port=8000, debug=True) + +This will add the blueprint to the application and register any routes defined +by that blueprint. In this example, the registered routes in the `app.router` +will look like: + +.. code-block:: python + + [Route(handler=, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')] + +Blueprint groups and nesting +---------------------------- + +Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example: + +| api/ +| β”œβ”€β”€content/ +| β”‚ β”œβ”€β”€authors.py +| β”‚ β”œβ”€β”€static.py +| β”‚ └──__init__.py +| β”œβ”€β”€info.py +| └──__init__.py +| app.py + +Initialization of this app's blueprint hierarchy could go as follows: + +.. code-block:: python + + # api/content/authors.py + from sanic import Blueprint + + authors = Blueprint('content_authors', url_prefix='/authors') + +.. code-block:: python + + # api/content/static.py + from sanic import Blueprint + + static = Blueprint('content_static', url_prefix='/static') + + +.. code-block:: python + + # api/content/__init__.py + from sanic import Blueprint + + from .static import static + from .authors import authors + + content = Blueprint.group(static, authors, url_prefix='/content') + +.. code-block:: python + + # api/info.py + from sanic import Blueprint + + info = Blueprint('info', url_prefix='/info') + +.. code-block:: python + + # api/__init__.py + from sanic import Blueprint + + from .content import content + from .info import info + + api = Blueprint.group(content, info, url_prefix='/api') + +And registering these blueprints in `app.py` can now be done like so: + +.. code-block:: python + + # app.py + from sanic import Sanic + + from .api import api + + app = Sanic(__name__) + + app.blueprint(api) + +Using Blueprints +---------------- + +Blueprints have almost the same functionality as an application instance. + +WebSocket routes +~~~~~~~~~~~~~~~~ + +WebSocket handlers can be registered on a blueprint using the `@bp.websocket` +decorator or `bp.add_websocket_route` method. + +Blueprint Middleware +~~~~~~~~~~~~~~~~~~~~ + +Using blueprints allows you to also register middleware globally. + +.. code-block:: python + + @bp.middleware + async def print_on_request(request): + print("I am a spy") + + @bp.middleware('request') + async def halt_request(request): + return text('I halted the request') + + @bp.middleware('response') + async def halt_response(request, response): + return text('I halted the response') + + +Blueprint Group Middleware +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the +current blueprint group under consideration. + +.. code-block:: python + + bp1 = Blueprint('bp1', url_prefix='/bp1') + bp2 = Blueprint('bp2', url_prefix='/bp2') + + @bp1.middleware('request') + async def bp1_only_middleware(request): + print('applied on Blueprint : bp1 Only') + + @bp1.route('/') + async def bp1_route(request): + return text('bp1') + + @bp2.route('/') + async def bp2_route(request, param): + return text(param) + + group = Blueprint.group(bp1, bp2) + + @group.middleware('request') + async def group_middleware(request): + print('common middleware applied for both bp1 and bp2') + + # Register Blueprint group under the app + app.blueprint(group) + +Exceptions +~~~~~~~~~~ + +Exceptions can be applied exclusively to blueprints globally. + +.. code-block:: python + + @bp.exception(NotFound) + def ignore_404s(request, exception): + return text("Yep, I totally found the page: {}".format(request.url)) + +Static files +~~~~~~~~~~~~ + +Static files can be served globally, under the blueprint prefix. + +.. code-block:: python + + # suppose bp.name == 'bp' + + bp.static('/web/path', '/folder/to/serve') + # also you can pass name parameter to it for url_for + bp.static('/web/path', '/folder/to/server', name='uploads') + app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt' + +Start and stop +-------------- + +Blueprints can run functions during the start and stop process of the server. +If running in multiprocessor mode (more than 1 worker), these are triggered +after the workers fork. + +Available events are: + +- `before_server_start`: Executed before the server begins to accept connections +- `after_server_start`: Executed after the server begins to accept connections +- `before_server_stop`: Executed before the server stops accepting connections +- `after_server_stop`: Executed after the server is stopped and all requests are complete + +.. code-block:: python + + bp = Blueprint('my_blueprint') + + @bp.listener('before_server_start') + async def setup_connection(app, loop): + global database + database = mysql.connect(host='127.0.0.1'...) + + @bp.listener('after_server_stop') + async def close_connection(app, loop): + await database.close() + +Use-case: API versioning +------------------------ + +Blueprints can be very useful for API versioning, where one blueprint may point +at `/v1/`, and another pointing at `/v2/`. + +When a blueprint is initialised, it can take an optional `version` argument, +which will be prepended to all routes defined on the blueprint. This feature +can be used to implement our API versioning scheme. + +.. code-block:: python + + # blueprints.py + from sanic.response import text + from sanic import Blueprint + + blueprint_v1 = Blueprint('v1', url_prefix='/api', version="v1") + blueprint_v2 = Blueprint('v2', url_prefix='/api', version="v2") + + @blueprint_v1.route('/') + async def api_v1_root(request): + return text('Welcome to version 1 of our documentation') + + @blueprint_v2.route('/') + async def api_v2_root(request): + return text('Welcome to version 2 of our documentation') + +When we register our blueprints on the app, the routes `/v1/api` and `/v2/api` will now +point to the individual blueprints, which allows the creation of *sub-sites* +for each API version. + +.. code-block:: python + + # main.py + from sanic import Sanic + from blueprints import blueprint_v1, blueprint_v2 + + app = Sanic(__name__) + app.blueprint(blueprint_v1) + app.blueprint(blueprint_v2) + + app.run(host='0.0.0.0', port=8000, debug=True) + +URL Building with `url_for` +--------------------------- + +If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name +takes the format `.`. For example: + +.. code-block:: python + + @blueprint_v1.route('/') + async def root(request): + url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5' + return redirect(url) + + + @blueprint_v1.route('/post/') + async def post_handler(request, post_id): + return text('Post {} in Blueprint V1'.format(post_id)) diff --git a/docs/sanic/class_based_views.md b/docs/sanic/class_based_views.md deleted file mode 100644 index c3304df6..00000000 --- a/docs/sanic/class_based_views.md +++ /dev/null @@ -1,166 +0,0 @@ -# Class-Based Views - -Class-based views are simply classes which implement response behaviour to -requests. They provide a way to compartmentalise handling of different HTTP -request types at the same endpoint. Rather than defining and decorating three -different handler functions, one for each of an endpoint's supported request -type, the endpoint can be assigned a class-based view. - -## Defining views - -A class-based view should subclass `HTTPMethodView`. You can then implement -class methods for every HTTP request type you want to support. If a request is -received that has no defined method, a `405: Method not allowed` response will -be generated. - -To register a class-based view on an endpoint, the `app.add_route` method is -used. The first argument should be the defined class with the method `as_view` -invoked, and the second should be the URL endpoint. - -The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class -using all these methods would look like the following. - -```python -from sanic import Sanic -from sanic.views import HTTPMethodView -from sanic.response import text - -app = Sanic('some_name') - -class SimpleView(HTTPMethodView): - - def get(self, request): - return text('I am get method') - - def post(self, request): - return text('I am post method') - - def put(self, request): - return text('I am put method') - - def patch(self, request): - return text('I am patch method') - - def delete(self, request): - return text('I am delete method') - -app.add_route(SimpleView.as_view(), '/') - -``` - -You can also use `async` syntax. - -```python -from sanic import Sanic -from sanic.views import HTTPMethodView -from sanic.response import text - -app = Sanic('some_name') - -class SimpleAsyncView(HTTPMethodView): - - async def get(self, request): - return text('I am async get method') - -app.add_route(SimpleAsyncView.as_view(), '/') - -``` - -## URL parameters - -If you need any URL parameters, as discussed in the routing guide, include them -in the method definition. - -```python -class NameView(HTTPMethodView): - - def get(self, request, name): - return text('Hello {}'.format(name)) - -app.add_route(NameView.as_view(), '/') -``` - -## Decorators - -If you want to add any decorators to the class, you can set the `decorators` -class variable. These will be applied to the class when `as_view` is called. - -```python -class ViewWithDecorator(HTTPMethodView): - decorators = [some_decorator_here] - - def get(self, request, name): - return text('Hello I have a decorator') - - def post(self, request, name): - return text("Hello I also have a decorator") - -app.add_route(ViewWithDecorator.as_view(), '/url') -``` - -But if you just want to decorate some functions and not all functions, you can do as follows: - -```python -class ViewWithSomeDecorator(HTTPMethodView): - - @staticmethod - @some_decorator_here - def get(request, name): - return text("Hello I have a decorator") - - def post(self, request, name): - return text("Hello I don't have any decorators") -``` - -## URL Building - -If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint -that you will pass into `url_for`. For example: - -```python -@app.route('/') -def index(request): - url = app.url_for('SpecialClassView') - return redirect(url) - - -class SpecialClassView(HTTPMethodView): - def get(self, request): - return text('Hello from the Special Class View!') - - -app.add_route(SpecialClassView.as_view(), '/special_class_view') -``` - - -## Using CompositionView - -As an alternative to the `HTTPMethodView`, you can use `CompositionView` to -move handler functions outside of the view class. - -Handler functions for each supported HTTP method are defined elsewhere in the -source, and then added to the view using the `CompositionView.add` method. The -first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`), -and the second is the handler function. The following example shows -`CompositionView` usage with both an external handler function and an inline -lambda: - -```python -from sanic import Sanic -from sanic.views import CompositionView -from sanic.response import text - -app = Sanic(__name__) - -def get_handler(request): - return text('I am a get method') - -view = CompositionView() -view.add(['GET'], get_handler) -view.add(['POST', 'PUT'], lambda request: text('I am a post/put method')) - -# Use the new view to handle requests to the base URL -app.add_route(view, '/') -``` - -Note: currently you cannot build a URL for a CompositionView using `url_for`. diff --git a/docs/sanic/class_based_views.rst b/docs/sanic/class_based_views.rst new file mode 100644 index 00000000..0170d734 --- /dev/null +++ b/docs/sanic/class_based_views.rst @@ -0,0 +1,169 @@ +Class-Based Views +================= + +Class-based views are simply classes which implement response behaviour to +requests. They provide a way to compartmentalise handling of different HTTP +request types at the same endpoint. Rather than defining and decorating three +different handler functions, one for each of an endpoint's supported request +type, the endpoint can be assigned a class-based view. + +Defining views +-------------- + +A class-based view should subclass `HTTPMethodView`. You can then implement +class methods for every HTTP request type you want to support. If a request is +received that has no defined method, a `405: Method not allowed` response will +be generated. + +To register a class-based view on an endpoint, the `app.add_route` method is +used. The first argument should be the defined class with the method `as_view` +invoked, and the second should be the URL endpoint. + +The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class +using all these methods would look like the following. + +.. code-block:: python + + from sanic import Sanic + from sanic.views import HTTPMethodView + from sanic.response import text + + app = Sanic('some_name') + + class SimpleView(HTTPMethodView): + + def get(self, request): + return text('I am get method') + + def post(self, request): + return text('I am post method') + + def put(self, request): + return text('I am put method') + + def patch(self, request): + return text('I am patch method') + + def delete(self, request): + return text('I am delete method') + + app.add_route(SimpleView.as_view(), '/') + +You can also use `async` syntax. + +.. code-block:: python + + from sanic import Sanic + from sanic.views import HTTPMethodView + from sanic.response import text + + app = Sanic('some_name') + + class SimpleAsyncView(HTTPMethodView): + + async def get(self, request): + return text('I am async get method') + + app.add_route(SimpleAsyncView.as_view(), '/') + +URL parameters +-------------- + +If you need any URL parameters, as discussed in the routing guide, include them +in the method definition. + +.. code-block:: python + + class NameView(HTTPMethodView): + + def get(self, request, name): + return text('Hello {}'.format(name)) + + app.add_route(NameView.as_view(), '/') + +Decorators +---------- + +If you want to add any decorators to the class, you can set the `decorators` +class variable. These will be applied to the class when `as_view` is called. + +.. code-block:: python + + class ViewWithDecorator(HTTPMethodView): + decorators = [some_decorator_here] + + def get(self, request, name): + return text('Hello I have a decorator') + + def post(self, request, name): + return text("Hello I also have a decorator") + + app.add_route(ViewWithDecorator.as_view(), '/url') + +But if you just want to decorate some functions and not all functions, you can do as follows: + +.. code-block:: python + + class ViewWithSomeDecorator(HTTPMethodView): + + @staticmethod + @some_decorator_here + def get(request, name): + return text("Hello I have a decorator") + + def post(self, request, name): + return text("Hello I don't have any decorators") + +URL Building +------------ + +If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint +that you will pass into `url_for`. For example: + +.. code-block:: python + + @app.route('/') + def index(request): + url = app.url_for('SpecialClassView') + return redirect(url) + + + class SpecialClassView(HTTPMethodView): + def get(self, request): + return text('Hello from the Special Class View!') + + + app.add_route(SpecialClassView.as_view(), '/special_class_view') + +Using CompositionView +--------------------- + +As an alternative to the `HTTPMethodView`, you can use `CompositionView` to +move handler functions outside of the view class. + +Handler functions for each supported HTTP method are defined elsewhere in the +source, and then added to the view using the `CompositionView.add` method. The +first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`), +and the second is the handler function. The following example shows +`CompositionView` usage with both an external handler function and an inline +lambda: + +.. code-block:: python + + from sanic import Sanic + from sanic.views import CompositionView + from sanic.response import text + + app = Sanic(__name__) + + def get_handler(request): + return text('I am a get method') + + view = CompositionView() + view.add(['GET'], get_handler) + view.add(['POST', 'PUT'], lambda request: text('I am a post/put method')) + + # Use the new view to handle requests to the base URL + app.add_route(view, '/') + +Note: currently you cannot build a URL for a CompositionView using `url_for`. diff --git a/docs/sanic/config.md b/docs/sanic/config.md deleted file mode 100644 index 26ddb4dd..00000000 --- a/docs/sanic/config.md +++ /dev/null @@ -1,211 +0,0 @@ -# Configuration - -Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations. - -## Basics - -Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary: - -``` -app = Sanic('myapp') -app.config.DB_NAME = 'appdb' -app.config.DB_USER = 'appuser' -``` - -Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once: - -``` -db_settings = { - 'DB_HOST': 'localhost', - 'DB_NAME': 'appdb', - 'DB_USER': 'appuser' -} -app.config.update(db_settings) -``` - -In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters. - -## Loading Configuration - -There are several ways how to load configuration. - -### From Environment Variables - -Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic: - -```python -app = Sanic(load_env='MYAPP_') -``` - -Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead: - -```python -app = Sanic(load_env=False) -``` - -### From an Object - -If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: - -``` -import myapp.default_settings - -app = Sanic('myapp') -app.config.from_object(myapp.default_settings) -``` -or also by path to config: - -``` -app = Sanic('myapp') -app.config.from_object('config.path.config.Class') -``` - - -You could use a class or any other object as well. - -### From a File - -Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file: - -``` -app = Sanic('myapp') -app.config.from_envvar('MYAPP_SETTINGS') -``` - -Then you can run your application with the `MYAPP_SETTINGS` environment variable set: - -``` -$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py -INFO: Goin' Fast @ http://0.0.0.0:8000 -``` - -The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs: - -``` -# config_file -DB_HOST = 'localhost' -DB_NAME = 'appdb' -DB_USER = 'appuser' -``` - -## Builtin Configuration Values - -Out of the box there are just a few predefined values which can be overwritten when creating the application. - - | Variable | Default | Description | - | ------------------------- | ----------------- | --------------------------------------------------------------------------- | - | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | - | REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size | - | REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) | - | RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) | - | KEEP_ALIVE | True | Disables keep-alive when False | - | KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) | - | GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) | - | ACCESS_LOG | True | Disable or enable access log | - | PROXIES_COUNT | -1 | The number of proxy servers in front of the app (e.g. nginx; see below) | - | FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip | - | REAL_IP_HEADER | "X-Real-IP" | The name of "X-Real-IP" HTTP header that contains real client ip | - -### The different Timeout variables: - -#### `REQUEST_TIMEOUT` - -A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the -Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the -`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response -and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads -or upload requests very slowly. - -#### `RESPONSE_TIMEOUT` - -A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the -Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` -value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the -client. Set this parameter's value higher if your application is likely to have long-running process that delay the -generation of a response. - -#### `KEEP_ALIVE_TIMEOUT` - -##### What is Keep Alive? And what does the Keep Alive Timeout value do? - -`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application) -can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response. -This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient -network traffic for both the client and the server. - -The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, -set it to `False` to cause all client connections to close immediately after a response is sent, regardless of -the `Keep-Alive` header on the request. - -The amount of time the server holds the TCP connection open is decided by the server itself. -In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds. -This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for -the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless -you know your clients are using a browser which supports TCP connections held open for that long. - -For reference: -``` -Apache httpd server default keepalive timeout = 5 seconds -Nginx server default keepalive timeout = 75 seconds -Nginx performance tuning guidelines uses keepalive = 15 seconds -IE (5-9) client hard keepalive limit = 60 seconds -Firefox client hard keepalive limit = 115 seconds -Opera 11 client hard keepalive limit = 120 seconds -Chrome 13+ client keepalive limit > 300+ seconds -``` - -### Proxy configuration - -When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy, typically `127.0.0.1`. Sanic may be configured to use proxy headers for determining the true client IP, available as `request.remote_addr`. The full external URL is also constructed from header fields if available. - -Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled. - -Services behind reverse proxies must configure `FORWARDED_SECRET`, `REAL_IP_HEADER` and/or `PROXIES_COUNT`. - -#### Forwarded header - -``` -Forwarded: for="1.2.3.4"; proto="https"; host="yoursite.com"; secret="Pr0xy", - for="10.0.0.1"; proto="http"; host="proxy.internal"; by="_1234proxy" -``` - -* Set `FORWARDED_SECRET` to an identifier used by the proxy of interest. - -The secret is used to securely identify a specific proxy server. Given the above header, secret `Pr0xy` would use the information on the first line and secret `_1234proxy` would use the second line. The secret must exactly match the value of `secret` or `by`. A secret in `by` must begin with an underscore and use only characters specified in [RFC 7239 section 6.3](https://tools.ietf.org/html/rfc7239#section-6.3), while `secret` has no such restrictions. - -Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set. - -All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client. - -#### Traditional proxy headers - -``` -X-Real-IP: 1.2.3.4 -X-Forwarded-For: 1.2.3.4, 10.0.0.1 -X-Forwarded-Proto: https -X-Forwarded-Host: yoursite.com -``` - -* Set `REAL_IP_HEADER` to `x-real-ip`, `true-client-ip`, `cf-connecting-ip` or other name of such header. -* Set `PROXIES_COUNT` to the number of entries expected in `x-forwarded-for` (name configurable via `FORWARDED_FOR_HEADER`). - -If client IP is found by one of these methods, Sanic uses the following headers for URL parts: - -* `x-forwarded-proto`, `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-path` and if necessary, `x-scheme`. - -#### Proxy config if using ... - -* a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header - * Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret` - * NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret` - * NGINX: after [the official instructions](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/), add anywhere in your config: - - proxy_set_header Forwarded "$proxy_add_forwarded;by=\"_$server_name\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\";secret=_secret"; - -* a custom header with client IP: set `REAL_IP_HEADER` to the name of that header -* `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP -* no proxies: no configuration required! - -#### Changes in Sanic 19.9 - -Earlier Sanic versions had unsafe default settings. From 19.9 onwards proxy settings must be set manually, and support for negative PROXIES_COUNT has been removed. diff --git a/docs/sanic/config.rst b/docs/sanic/config.rst new file mode 100644 index 00000000..0e34bcc7 --- /dev/null +++ b/docs/sanic/config.rst @@ -0,0 +1,242 @@ +Configuration +============= + +Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations. + +Basics +------ + +Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary: + +.. code-block:: python + + app = Sanic('myapp') + app.config.DB_NAME = 'appdb' + app.config.DB_USER = 'appuser' + +Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once: + +.. code-block:: python + + db_settings = { + 'DB_HOST': 'localhost', + 'DB_NAME': 'appdb', + 'DB_USER': 'appuser' + } + app.config.update(db_settings) + +In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters. + +Loading Configuration +--------------------- + +There are several ways how to load configuration. + +From Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic: + +.. code-block:: python + + app = Sanic(load_env='MYAPP_') + +Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead: + +.. code-block:: python + + app = Sanic(load_env=False) + +From an Object +~~~~~~~~~~~~~~ + +If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: + +.. code-block:: python + + import myapp.default_settings + + app = Sanic('myapp') + app.config.from_object(myapp.default_settings) + +or also by path to config: + +.. code-block:: python + + app = Sanic('myapp') + app.config.from_object('config.path.config.Class') + +You could use a class or any other object as well. + +From a File +~~~~~~~~~~~ + +Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file: + +.. code-block:: python + + app = Sanic('myapp') + app.config.from_envvar('MYAPP_SETTINGS') + +Then you can run your application with the `MYAPP_SETTINGS` environment variable set: + +.. code-block:: python + + #$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py + #INFO: Goin' Fast @ http://0.0.0.0:8000 + + +The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs: + +.. code-block:: python + + # config_file + DB_HOST = 'localhost' + DB_NAME = 'appdb' + DB_USER = 'appuser' + +Builtin Configuration Values +---------------------------- + +Out of the box there are just a few predefined values which can be overwritten when creating the application. + ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| Variable | Default | Description | ++===========================+===================+=============================================================================+ +| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| KEEP_ALIVE | True | Disables keep-alive when False | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| ACCESS_LOG | True | Disable or enable access log | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| PROXIES_COUNT | -1 | The number of proxy servers in front of the app (e.g. nginx; see below) | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ +| REAL_IP_HEADER | "X-Real-IP" | The name of "X-Real-IP" HTTP header that contains real client ip | ++---------------------------+-------------------+-----------------------------------------------------------------------------+ + +The different Timeout variables: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`REQUEST_TIMEOUT` +################# + +A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the +Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the +`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response +and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads +or upload requests very slowly. + +`RESPONSE_TIMEOUT` +################## + +A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the +Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` +value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the +client. Set this parameter's value higher if your application is likely to have long-running process that delay the +generation of a response. + +`KEEP_ALIVE_TIMEOUT` +#################### + +What is Keep Alive? And what does the Keep Alive Timeout value do? +****************************************************************** + +`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application) +can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response. +This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient +network traffic for both the client and the server. + +The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, +set it to `False` to cause all client connections to close immediately after a response is sent, regardless of +the `Keep-Alive` header on the request. + +The amount of time the server holds the TCP connection open is decided by the server itself. +In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds. +This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for +the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless +you know your clients are using a browser which supports TCP connections held open for that long. + +For reference: + +* Apache httpd server default keepalive timeout = 5 seconds +* Nginx server default keepalive timeout = 75 seconds +* Nginx performance tuning guidelines uses keepalive = 15 seconds +* IE (5-9) client hard keepalive limit = 60 seconds +* Firefox client hard keepalive limit = 115 seconds +* Opera 11 client hard keepalive limit = 120 seconds +* Chrome 13+ client keepalive limit > 300+ seconds + + +Proxy configuration +~~~~~~~~~~~~~~~~~~~ + +When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy, +typically `127.0.0.1`. Sanic may be configured to use proxy headers for determining the true client IP, +available as `request.remote_addr`. The full external URL is also constructed from header fields if available. + +Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled. + +Services behind reverse proxies must configure `FORWARDED_SECRET`, `REAL_IP_HEADER` and/or `PROXIES_COUNT`. + +Forwarded header +################ + +.. Forwarded: for="1.2.3.4"; proto="https"; host="yoursite.com"; secret="Pr0xy", for="10.0.0.1"; proto="http"; host="proxy.internal"; by="_1234proxy" + +* Set `FORWARDED_SECRET` to an identifier used by the proxy of interest. + +The secret is used to securely identify a specific proxy server. Given the above header, secret `Pr0xy` would use the +information on the first line and secret `_1234proxy` would use the second line. The secret must exactly match the value +of `secret` or `by`. A secret in `by` must begin with an underscore and use only characters specified in +`RFC 7239 section 6.3 `_, while `secret` has no such restrictions. + +Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set. + +All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client. + +Traditional proxy headers +######################### + +.. X-Real-IP: 1.2.3.4 + X-Forwarded-For: 1.2.3.4, 10.0.0.1 + X-Forwarded-Proto: https + X-Forwarded-Host: yoursite.com + + +* Set `REAL_IP_HEADER` to `x-real-ip`, `true-client-ip`, `cf-connecting-ip` or other name of such header. +* Set `PROXIES_COUNT` to the number of entries expected in `x-forwarded-for` (name configurable via `FORWARDED_FOR_HEADER`). + +If client IP is found by one of these methods, Sanic uses the following headers for URL parts: + +* `x-forwarded-proto`, `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-path` and if necessary, `x-scheme`. + +Proxy config if using ... +######################### + +* a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header + * Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret` + * NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret` + * NGINX: after `the official instructions `_, add anywhere in your config: + +.. proxy_set_header Forwarded "$proxy_add_forwarded;by=\"_$server_name\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\";secret=_secret"; + +* a custom header with client IP: set `REAL_IP_HEADER` to the name of that header +* `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP +* no proxies: no configuration required! + +Changes in Sanic 19.9 +##################### + +Earlier Sanic versions had unsafe default settings. From 19.9 onwards proxy settings must be set manually, and support for negative PROXIES_COUNT has been removed. diff --git a/docs/sanic/debug_mode.rst b/docs/sanic/debug_mode.rst index 14356855..2c79a6bf 100644 --- a/docs/sanic/debug_mode.rst +++ b/docs/sanic/debug_mode.rst @@ -13,7 +13,7 @@ and by default will enable the Auto Reload feature. Setting the debug mode ---------------------- -By setting the ``debug`` mode a more verbose output from Sanic will be outputed +By setting the ``debug`` mode a more verbose output from Sanic will be output and the Automatic Reloader will be activated. .. code-block:: python @@ -50,4 +50,4 @@ the ``auto_reload`` argument will activate or deactivate the Automatic Reloader. return json({"hello": "world"}) if __name__ == '__main__': - app.run(host="0.0.0.0", port=8000, auto_reload=True) \ No newline at end of file + app.run(host="0.0.0.0", port=8000, auto_reload=True) diff --git a/docs/sanic/decorators.md b/docs/sanic/decorators.md deleted file mode 100644 index 68495a22..00000000 --- a/docs/sanic/decorators.md +++ /dev/null @@ -1,39 +0,0 @@ -# Handler Decorators - -Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed. - -## Authorization Decorator - -Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response. - - -```python -from functools import wraps -from sanic.response import json - -def authorized(): - def decorator(f): - @wraps(f) - async def decorated_function(request, *args, **kwargs): - # run some method that checks the request - # for the client's authorization status - is_authorized = check_request_for_authorization_status(request) - - if is_authorized: - # the user is authorized. - # run the handler method and return the response - response = await f(request, *args, **kwargs) - return response - else: - # the user is not authorized. - return json({'status': 'not_authorized'}, 403) - return decorated_function - return decorator - - -@app.route("/") -@authorized() -async def test(request): - return json({'status': 'authorized'}) -``` - diff --git a/docs/sanic/decorators.rst b/docs/sanic/decorators.rst new file mode 100644 index 00000000..2351b3bc --- /dev/null +++ b/docs/sanic/decorators.rst @@ -0,0 +1,40 @@ +Handler Decorators +================== + +Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed. + +Authorization Decorator +----------------------- + +Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response. + + +.. code-block:: python + + from functools import wraps + from sanic.response import json + + def authorized(): + def decorator(f): + @wraps(f) + async def decorated_function(request, *args, **kwargs): + # run some method that checks the request + # for the client's authorization status + is_authorized = check_request_for_authorization_status(request) + + if is_authorized: + # the user is authorized. + # run the handler method and return the response + response = await f(request, *args, **kwargs) + return response + else: + # the user is not authorized. + return json({'status': 'not_authorized'}, 403) + return decorated_function + return decorator + + + @app.route("/") + @authorized() + async def test(request): + return json({'status': 'authorized'}) diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.rst similarity index 53% rename from docs/sanic/deploying.md rename to docs/sanic/deploying.rst index b72750db..a0b4f1bb 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.rst @@ -1,10 +1,12 @@ -# Deploying +Deploying +========= Deploying Sanic is very simple using one of three options: the inbuilt webserver, -an [ASGI webserver](https://asgi.readthedocs.io/en/latest/implementations.html), or `gunicorn`. -It is also very common to place Sanic behind a reverse proxy, like `nginx`. +an `ASGI webserver `_, or `gunicorn`. +It is also very common to place Sanic behind a reverse proxy, like `nginx`. -## Running via Sanic webserver +Running via Sanic webserver +--------------------------- After defining an instance of `sanic.Sanic`, we can call the `run` method with the following keyword arguments: @@ -15,136 +17,144 @@ keyword arguments: - `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s). - `sock` *(default `None`)*: Socket for the server to accept connections from. - `workers` *(default `1`)*: Number of worker processes to spawn. -- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is - specified, Sanic creates its own event loop. -- `protocol` *(default `HttpProtocol`)*: Subclass - of - [asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). +- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is specified, Sanic creates its own event loop. +- `protocol` *(default `HttpProtocol`)*: Subclass of `asyncio.protocol `_. - `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server). -```python -app.run(host='0.0.0.0', port=1337, access_log=False) -``` +.. code-block:: python + + app.run(host='0.0.0.0', port=1337, access_log=False) In the above example, we decided to turn off the access log in order to increase performance. -### Workers +Workers +~~~~~~~ By default, Sanic listens in the main process using only one CPU core. To crank up the juice, just specify the number of workers in the `run` arguments. -```python -app.run(host='0.0.0.0', port=1337, workers=4) -``` +.. code-block:: python + + app.run(host='0.0.0.0', port=1337, workers=4) Sanic will automatically spin up multiple processes and route traffic between them. We recommend as many workers as you have available cores. -### Running via command +Running via command +~~~~~~~~~~~~~~~~~~~ If you like using command line arguments, you can launch a Sanic webserver by executing the module. For example, if you initialized Sanic as `app` in a file named `server.py`, you could run the server like so: -`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4` +.. python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4 With this way of running sanic, it is not necessary to invoke `app.run` in your Python file. If you do, make sure you wrap it so that it only executes when directly run by the interpreter. -```python -if __name__ == '__main__': - app.run(host='0.0.0.0', port=1337, workers=4) -``` +.. code-block:: python -## Running via ASGI + if __name__ == '__main__': + app.run(host='0.0.0.0', port=1337, workers=4) + +Running via ASGI +---------------- Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver to run Sanic. The three main implementations of ASGI are -[Daphne](http://github.com/django/daphne), [Uvicorn](https://www.uvicorn.org/), -and [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html). +`Daphne `_, `Uvicorn `_, +and `Hypercorn `_. Follow their documentation for the proper way to run them, but it should look something like: -``` -daphne myapp:app -uvicorn myapp:app -hypercorn myapp:app -``` +:: + + daphne myapp:app + uvicorn myapp:app + hypercorn myapp:app A couple things to note when using ASGI: -1. When using the Sanic webserver, websockets will run using the [`websockets`](https://websockets.readthedocs.io/) package. In ASGI mode, there is no need for this package since websockets are managed in the ASGI server. -1. The ASGI [lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) supports -only two server events: startup and shutdown. Sanic has four: before startup, after startup, -before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will -run consecutively and not actually around the server process beginning and ending (since that -is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and +1. When using the Sanic webserver, websockets will run using the `websockets `_ package. +In ASGI mode, there is no need for this package since websockets are managed in the ASGI server. +2. The ASGI `lifespan protocol `, supports +only two server events: startup and shutdown. Sanic has four: before startup, after startup, +before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will +run consecutively and not actually around the server process beginning and ending (since that +is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and `before_server_stop`. -1. ASGI mode is still in "beta" as of Sanic v19.6. +3. ASGI mode is still in "beta" as of Sanic v19.6. -## Running via Gunicorn +Running via Gunicorn +-------------------- -[Gunicorn](http://gunicorn.org/) β€˜Green Unicorn’ is a WSGI HTTP Server for UNIX. +`Gunicorn `_ β€˜Green Unicorn’ is a WSGI HTTP Server for UNIX. It’s a pre-fork worker model ported from Ruby’s Unicorn project. In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker` for Gunicorn `worker-class` argument: -``` -gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker -``` +:: + + gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker + If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker after it has processed a given number of requests. This can be a convenient way to help limit the effects of the memory leak. -See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information. +See the `Gunicorn Docs `_ for more information. -## Other deployment considerations +Other deployment considerations +------------------------------- -### Running behind a reverse proxy +Running behind a reverse proxy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration: -``` -server { - listen 80; - server_name example.org; - location / { - proxy_pass http://127.0.0.1:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} -``` +:: + + server { + listen 80; + server_name example.org; + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } + If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information. -### Disable debug logging for performance +Disable debug logging for performance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To improve the performance add `debug=False` and `access_log=False` in the `run` arguments. -```python -app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False) -``` +.. code-block:: python + + app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False) Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"` -``` -env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning -``` +:: + + env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning Or you can rewrite app config directly -```python -app.config.ACCESS_LOG = False -``` +.. code-block:: python -### Asynchronous support and sharing the loop + app.config.ACCESS_LOG = False + +Asynchronous support and sharing the loop +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is suitable if you *need* to share the Sanic process with other applications, in particular the `loop`. However, be advised that this method does not support using multiple processes, and is not the preferred way @@ -152,12 +162,12 @@ to run the app in general. Here is an incomplete example (please see `run_async.py` in examples for something more practical): -```python -server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True) -loop = asyncio.get_event_loop() -task = asyncio.ensure_future(server) -loop.run_forever() -``` +.. code-block:: python + + server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True) + loop = asyncio.get_event_loop() + task = asyncio.ensure_future(server) + loop.run_forever() Caveat: using this method, calling `app.create_server()` will trigger "before_server_start" server events, but not "after_server_start", "before_server_stop", or "after_server_stop" server events. @@ -167,25 +177,25 @@ the server task. Here is an incomplete example (please see `run_async_advanced.py` in examples for something more complete): -```python -serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True) -loop = asyncio.get_event_loop() -serv_task = asyncio.ensure_future(serv_coro, loop=loop) -server = loop.run_until_complete(serv_task) -server.after_start() -try: - loop.run_forever() -except KeyboardInterrupt as e: - loop.stop() -finally: - server.before_stop() +.. code-block:: python - # Wait for server to close - close_task = server.close() - loop.run_until_complete(close_task) + serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True) + loop = asyncio.get_event_loop() + serv_task = asyncio.ensure_future(serv_coro, loop=loop) + server = loop.run_until_complete(serv_task) + server.after_start() + try: + loop.run_forever() + except KeyboardInterrupt as e: + loop.stop() + finally: + server.before_stop() - # Complete all tasks on the loop - for connection in server.connections: - connection.close_if_idle() - server.after_stop() -``` \ No newline at end of file + # Wait for server to close + close_task = server.close() + loop.run_until_complete(close_task) + + # Complete all tasks on the loop + for connection in server.connections: + connection.close_if_idle() + server.after_stop() diff --git a/docs/sanic/exceptions.md b/docs/sanic/exceptions.md deleted file mode 100644 index 4ddf5b2d..00000000 --- a/docs/sanic/exceptions.md +++ /dev/null @@ -1,88 +0,0 @@ -# Exceptions - -Exceptions can be thrown from within request handlers and will automatically be -handled by Sanic. Exceptions take a message as their first argument, and can -also take a status code to be passed back in the HTTP response. - -## Throwing an exception - -To throw an exception, simply `raise` the relevant exception from the -`sanic.exceptions` module. - -```python -from sanic.exceptions import ServerError - -@app.route('/killme') -async def i_am_ready_to_die(request): - raise ServerError("Something bad happened", status_code=500) -``` - -You can also use the `abort` function with the appropriate status code: - -```python -from sanic.exceptions import abort -from sanic.response import text - -@app.route('/youshallnotpass') -async def no_no(request): - abort(401) - # this won't happen - text("OK") -``` - -## Handling exceptions - -To override Sanic's default handling of an exception, the `@app.exception` -decorator is used. The decorator expects a list of exceptions to handle as -arguments. You can pass `SanicException` to catch them all! The decorated -exception handler function must take a `Request` and `Exception` object as -arguments. - -```python -from sanic.response import text -from sanic.exceptions import NotFound - -@app.exception(NotFound) -async def ignore_404s(request, exception): - return text("Yep, I totally found the page: {}".format(request.url)) -``` - -You can also add an exception handler as such: - -```python -from sanic import Sanic - -async def server_error_handler(request, exception): - return text("Oops, server error", status=500) - -app = Sanic() -app.error_handler.add(Exception, server_error_handler) -``` - -In some cases, you might want to add some more error handling -functionality to what is provided by default. In that case, you -can subclass Sanic's default error handler as such: - -```python -from sanic import Sanic -from sanic.handlers import ErrorHandler - -class CustomErrorHandler(ErrorHandler): - def default(self, request, exception): - ''' handles errors that have no error handlers assigned ''' - # You custom error handling logic... - return super().default(request, exception) - -app = Sanic() -app.error_handler = CustomErrorHandler() -``` - -## Useful exceptions - -Some of the most useful exceptions are presented below: - -- `NotFound`: called when a suitable route for the request isn't found. -- `ServerError`: called when something goes wrong inside the server. This - usually occurs if there is an exception raised in user code. - -See the `sanic.exceptions` module for the full list of exceptions to throw. diff --git a/docs/sanic/exceptions.rst b/docs/sanic/exceptions.rst new file mode 100644 index 00000000..0baaef8c --- /dev/null +++ b/docs/sanic/exceptions.rst @@ -0,0 +1,92 @@ +Exceptions +========== + +Exceptions can be thrown from within request handlers and will automatically be +handled by Sanic. Exceptions take a message as their first argument, and can +also take a status code to be passed back in the HTTP response. + +Throwing an exception +--------------------- + +To throw an exception, simply `raise` the relevant exception from the +`sanic.exceptions` module. + +.. code-block:: python + + from sanic.exceptions import ServerError + + @app.route('/killme') + async def i_am_ready_to_die(request): + raise ServerError("Something bad happened", status_code=500) + +You can also use the `abort` function with the appropriate status code: + +.. code-block:: python + + from sanic.exceptions import abort + from sanic.response import text + + @app.route('/youshallnotpass') + async def no_no(request): + abort(401) + # this won't happen + text("OK") + +Handling exceptions +------------------- + +To override Sanic's default handling of an exception, the `@app.exception` +decorator is used. The decorator expects a list of exceptions to handle as +arguments. You can pass `SanicException` to catch them all! The decorated +exception handler function must take a `Request` and `Exception` object as +arguments. + +.. code-block:: python + + from sanic.response import text + from sanic.exceptions import NotFound + + @app.exception(NotFound) + async def ignore_404s(request, exception): + return text("Yep, I totally found the page: {}".format(request.url)) + +You can also add an exception handler as such: + +.. code-block:: python + + from sanic import Sanic + + async def server_error_handler(request, exception): + return text("Oops, server error", status=500) + + app = Sanic() + app.error_handler.add(Exception, server_error_handler) + +In some cases, you might want to add some more error handling +functionality to what is provided by default. In that case, you +can subclass Sanic's default error handler as such: + +.. code-block:: python + + from sanic import Sanic + from sanic.handlers import ErrorHandler + + class CustomErrorHandler(ErrorHandler): + def default(self, request, exception): + ''' handles errors that have no error handlers assigned ''' + # You custom error handling logic... + return super().default(request, exception) + + app = Sanic() + app.error_handler = CustomErrorHandler() + +Useful exceptions +----------------- + +Some of the most useful exceptions are presented below: + +- `NotFound`: called when a suitable route for the request isn't found. +- `ServerError`: called when something goes wrong inside the server. This + usually occurs if there is an exception raised in user code. + +See the `sanic.exceptions` module for the full list of exceptions to throw. diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md deleted file mode 100644 index b13201af..00000000 --- a/docs/sanic/extensions.md +++ /dev/null @@ -1,3 +0,0 @@ -# Extensions - -Moved to the [awesome-sanic](https://github.com/mekicha/awesome-sanic) list. \ No newline at end of file diff --git a/docs/sanic/extensions.rst b/docs/sanic/extensions.rst new file mode 100644 index 00000000..25990c7f --- /dev/null +++ b/docs/sanic/extensions.rst @@ -0,0 +1,4 @@ +Extensions +========== + +Moved to the `awesome-sanic `_ list. diff --git a/docs/sanic/getting_started.md b/docs/sanic/getting_started.md deleted file mode 100644 index c6a688a4..00000000 --- a/docs/sanic/getting_started.md +++ /dev/null @@ -1,57 +0,0 @@ -# Getting Started - -Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at -least version 3.6 of Python before starting. Sanic uses the new `async`/`await` -syntax, so earlier versions of python won't work. - -## 1. Install Sanic - -> If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency. - -```bash -pip3 install sanic -``` - -To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables -using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`) -to true will stop that features installation. - -```bash -SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic -``` - -You can also install Sanic from [`conda-forge`](https://anaconda.org/conda-forge/sanic) - -```bash -conda config --add channels conda-forge -conda install sanic -``` - -## 2. Create a file called `main.py` - - ```python - from sanic import Sanic - from sanic.response import json - - app = Sanic() - - @app.route("/") - async def test(request): - return json({"hello": "world"}) - - if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000) - ``` - -## 3. Run the server - - ``` - python3 main.py - ``` - -## 4. Check your browser - -Open the address `http://0.0.0.0:8000` in your web browser. You should see -the message *Hello world!*. - -You now have a working Sanic server! diff --git a/docs/sanic/getting_started.rst b/docs/sanic/getting_started.rst new file mode 100644 index 00000000..861bb722 --- /dev/null +++ b/docs/sanic/getting_started.rst @@ -0,0 +1,62 @@ +Getting Started +=============== + +Make sure you have both `pip `_ and at +least version 3.6 of Python before starting. Sanic uses the new `async`/`await` +syntax, so earlier versions of python won't work. + +1. Install Sanic +---------------- + +> If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency. + +.. code-block:: bash + + pip3 install sanic + +To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables +using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`) +to true will stop that features installation. + +.. code-block:: bash + + SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic + +You can also install Sanic from `conda-forge `_ + +.. code-block:: bash + + conda config --add channels conda-forge + conda install sanic + +2. Create a file called `main.py` +--------------------------------- + +.. code-block:: python + + from sanic import Sanic + from sanic.response import json + + app = Sanic() + + @app.route("/") + async def test(request): + return json({"hello": "world"}) + + if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) + +3. Run the server +----------------- + +.. code-block:: bash + + python3 main.py + +4. Check your browser +--------------------- + +Open the address `http://0.0.0.0:8000 `_ in your web browser. You should see +the message *Hello world!*. + +You now have a working Sanic server! diff --git a/docs/sanic/index.rst b/docs/sanic/index.rst index a76f5884..fb4f3ae2 100644 --- a/docs/sanic/index.rst +++ b/docs/sanic/index.rst @@ -26,4 +26,5 @@ Sanic aspires to be simple .. note:: - Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru December 2020. Official Python support for version 3.5 is set to expire in September 2020. \ No newline at end of file + Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru + December 2020. Official Python support for version 3.5 is set to expire in September 2020. diff --git a/docs/sanic/middleware.md b/docs/sanic/middleware.rst similarity index 50% rename from docs/sanic/middleware.md rename to docs/sanic/middleware.rst index f4c634e2..3ab27dab 100644 --- a/docs/sanic/middleware.md +++ b/docs/sanic/middleware.rst @@ -1,4 +1,5 @@ -# Middleware And Listeners +Middleware And Listeners +======================== Middleware are functions which are executed before or after requests to the server. They can be used to modify the *request to* or *response from* @@ -6,7 +7,8 @@ user-defined handler functions. Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle. -## Middleware +Middleware +---------- There are two types of middleware: request and response. Both are declared using the `@app.middleware` decorator, with the decorator's parameter being a @@ -17,76 +19,78 @@ string representing its type: `'request'` or `'response'`. The simplest middleware doesn't modify the request or response at all: -``` -@app.middleware('request') -async def print_on_request(request): - print("I print when a request is received by the server") +.. code-block:: python -@app.middleware('response') -async def print_on_response(request, response): - print("I print when a response is returned by the server") -``` + @app.middleware('request') + async def print_on_request(request): + print("I print when a request is received by the server") -## Modifying the request or response + @app.middleware('response') + async def print_on_response(request, response): + print("I print when a response is returned by the server") + +Modifying the request or response +--------------------------------- Middleware can modify the request or response parameter it is given, *as long as it does not return it*. The following example shows a practical use-case for this. -``` -app = Sanic(__name__) +.. code-block:: python + + app = Sanic(__name__) -@app.middleware('request') -async def add_key(request): - # Arbitrary data may be stored in request context: - request.ctx.foo = 'bar' + @app.middleware('request') + async def add_key(request): + # Arbitrary data may be stored in request context: + request.ctx.foo = 'bar' -@app.middleware('response') -async def custom_banner(request, response): - response.headers["Server"] = "Fake-Server" + @app.middleware('response') + async def custom_banner(request, response): + response.headers["Server"] = "Fake-Server" -@app.middleware('response') -async def prevent_xss(request, response): - response.headers["x-xss-protection"] = "1; mode=block" + @app.middleware('response') + async def prevent_xss(request, response): + response.headers["x-xss-protection"] = "1; mode=block" -@app.get("/") -async def index(request): - return sanic.response.text(request.ctx.foo) + @app.get("/") + async def index(request): + return sanic.response.text(request.ctx.foo) -app.run(host="0.0.0.0", port=8000) -``` + app.run(host="0.0.0.0", port=8000) The three middlewares are executed in order: 1. The first request middleware **add_key** adds a new key `foo` into request context. 2. Request is routed to handler **index**, which gets the key from context and returns a text response. -3. The first response middleware **custom_banner** changes the HTTP response header *Server* to -say *Fake-Server* +3. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server* 4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks. -## Responding early +Responding early +---------------- If middleware returns a `HTTPResponse` object, the request will stop processing and the response will be returned. If this occurs to a request before the relevant user route handler is reached, the handler will never be called. Returning a response will also prevent any further middleware from running. -``` -@app.middleware('request') -async def halt_request(request): - return text('I halted the request') +.. code-block:: python -@app.middleware('response') -async def halt_response(request, response): - return text('I halted the response') -``` + @app.middleware('request') + async def halt_request(request): + return text('I halted the request') -## Custom context + @app.middleware('response') + async def halt_response(request, response): + return text('I halted the response') + +Custom context +-------------- Arbitrary data may be stored in `request.ctx`. A typical use case would be to store the user object acquired from database in an authentication @@ -96,7 +100,8 @@ the handler over the duration of the request. Custom context is reserved for applications and extensions. Sanic itself makes no use of it. -## Listeners +Listeners +--------- If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners: @@ -109,65 +114,64 @@ These listeners are implemented as decorators on functions which accept the app For example: -``` -@app.listener('before_server_start') -async def setup_db(app, loop): - app.db = await db_setup() +.. code-block:: python -@app.listener('after_server_start') -async def notify_server_started(app, loop): - print('Server successfully started!') + @app.listener('before_server_start') + async def setup_db(app, loop): + app.db = await db_setup() -@app.listener('before_server_stop') -async def notify_server_stopping(app, loop): - print('Server shutting down!') + @app.listener('after_server_start') + async def notify_server_started(app, loop): + print('Server successfully started!') -@app.listener('after_server_stop') -async def close_db(app, loop): - await app.db.close() -``` + @app.listener('before_server_stop') + async def notify_server_stopping(app, loop): + print('Server shutting down!') + + @app.listener('after_server_stop') + async def close_db(app, loop): + await app.db.close() It's also possible to register a listener using the `register_listener` method. This may be useful if you define your listeners in another module besides the one you instantiate your app in. -``` -app = Sanic() +.. code-block:: python -async def setup_db(app, loop): - app.db = await db_setup() + app = Sanic() -app.register_listener(setup_db, 'before_server_start') + async def setup_db(app, loop): + app.db = await db_setup() -``` + app.register_listener(setup_db, 'before_server_start') If you want to schedule a background task to run after the loop has started, Sanic provides the `add_task` method to easily do so. -``` -async def notify_server_started_after_five_seconds(): - await asyncio.sleep(5) - print('Server successfully started!') +.. code-block:: python -app.add_task(notify_server_started_after_five_seconds()) -``` + async def notify_server_started_after_five_seconds(): + await asyncio.sleep(5) + print('Server successfully started!') + + app.add_task(notify_server_started_after_five_seconds()) Sanic will attempt to automatically inject the app, passing it as an argument to the task: -``` -async def notify_server_started_after_five_seconds(app): - await asyncio.sleep(5) - print(app.name) +.. code-block:: python -app.add_task(notify_server_started_after_five_seconds) -``` + async def notify_server_started_after_five_seconds(app): + await asyncio.sleep(5) + print(app.name) + + app.add_task(notify_server_started_after_five_seconds) Or you can pass the app explicitly for the same effect: -``` -async def notify_server_started_after_five_seconds(app): - await asyncio.sleep(5) - print(app.name) +.. code-block:: python -app.add_task(notify_server_started_after_five_seconds(app)) -` + async def notify_server_started_after_five_seconds(app): + await asyncio.sleep(5) + print(app.name) + + app.add_task(notify_server_started_after_five_seconds(app)) diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.rst similarity index 59% rename from docs/sanic/request_data.md rename to docs/sanic/request_data.rst index eb8f12fa..7c32fa9b 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.rst @@ -1,4 +1,5 @@ -# Request Data +Request Data +============ When an endpoint receives a HTTP request, the route function is passed a `Request` object. @@ -7,50 +8,49 @@ The following variables are accessible as properties on `Request` objects: - `json` (any) - JSON body - ```python - from sanic.response import json +.. code-block:: python - @app.route("/json") - def post_json(request): + from sanic.response import json + + @app.route("/json") + def post_json(request): return json({ "received": True, "message": request.json }) - ``` + - `args` (dict) - Query string variables. A query string is the section of a - URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed, - the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`. - The request's `query_string` variable holds the unparsed string value. - Property is providing the default parsing strategy. If you would like to change it look to the section below - (`Changing the default parsing rules of the queryset`). + URL that resembles ``?key1=value1&key2=value2``. +If that URL were to be parsed, the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`. +The request's `query_string` variable holds the unparsed string value. Property is providing the default parsing +strategy. If you would like to change it look to the section below (`Changing the default parsing rules of the queryset`). - ```python - from sanic.response import json +.. code-block:: python - @app.route("/query_string") - def query_string(request): + from sanic.response import json + + @app.route("/query_string") + def query_string(request): return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) - ``` - `query_args` (list) - On many cases you would need to access the url arguments in a less packed form. `query_args` is the list of `(key, value)` tuples. - Property is providing the default parsing strategy. If you would like to change it look to the section below - (`Changing the default parsing rules of the queryset`). - For the same previous URL queryset `?key1=value1&key2=value2`, the - `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`. - And in case of the multiple params with the same key like `?key1=value1&key2=value2&key1=value3` - the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`. +Property is providing the default parsing strategy. If you would like to change it look to the section below +(`Changing the default parsing rules of the queryset`). For the same previous URL queryset `?key1=value1&key2=value2`, +the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`. And in case of the multiple params +with the same key like `?key1=value1&key2=value2&key1=value3` the `query_args` list would look like +`[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`. - The difference between Request.args and Request.query_args - for the queryset `?key1=value1&key2=value2&key1=value3` +The difference between Request.args and Request.query_args for the queryset `?key1=value1&key2=value2&key1=value3` - ```python - from sanic import Sanic - from sanic.response import json +.. code-block:: python - app = Sanic(__name__) + from sanic import Sanic + from sanic.response import json + + app = Sanic(__name__) - @app.route("/test_request_args") - async def test_request_args(request): + @app.route("/test_request_args") + async def test_request_args(request): return json({ "parsed": True, "url": request.url, @@ -60,32 +60,32 @@ The following variables are accessible as properties on `Request` objects: "query_args": request.query_args, }) - if __name__ == '__main__': + if __name__ == '__main__': app.run(host="0.0.0.0", port=8000) - ``` Output - ``` - { - "parsed":true, - "url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3", - "query_string":"key1=value1&key2=value2&key1=value3", - "args":{"key1":["value1","value3"],"key2":["value2"]}, - "raw_args":{"key1":"value1","key2":"value2"}, - "query_args":[["key1","value1"],["key2","value2"],["key1","value3"]] - } - ``` +.. code-block:: json - `raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions. + { + "parsed":true, + "url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3", + "query_string":"key1=value1&key2=value2&key1=value3", + "args":{"key1":["value1","value3"],"key2":["value2"]}, + "raw_args":{"key1":"value1","key2":"value2"}, + "query_args":[["key1","value1"],["key2","value2"],["key1","value3"]] + } + +- `raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions. - `files` (dictionary of `File` objects) - List of files that have a name, body, and type - ```python - from sanic.response import json +.. code-block:: python - @app.route("/files") - def post_json(request): + from sanic.response import json + + @app.route("/files") + def post_json(request): test_file = request.files.get('test') file_parameters = { @@ -95,28 +95,28 @@ The following variables are accessible as properties on `Request` objects: } return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters }) - ``` - `form` (dict) - Posted form variables. - ```python - from sanic.response import json +.. code-block:: python - @app.route("/form") - def post_json(request): + from sanic.response import json + + @app.route("/form") + def post_json(request): return json({ "received": True, "form_data": request.form, "test": request.form.get('test') }) - ``` - `body` (bytes) - Posted raw body. This property allows retrieval of the request's raw data, regardless of content type. - ```python +.. code-block:: python + from sanic.response import text @app.route("/users", methods=["POST",]) def create_user(request): return text("You are trying to create a user with the following POST: %s" % request.body) - ``` + - `headers` (dict) - A case-insensitive dictionary that contains the request headers. @@ -130,7 +130,8 @@ The following variables are accessible as properties on `Request` objects: - `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object. - ```python +.. code-block:: python + from sanic.response import json from sanic import Blueprint @@ -143,7 +144,6 @@ The following variables are accessible as properties on `Request` objects: else: return json({'status': 'production'}) - ``` - `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar` - `scheme`: The URL scheme associated with the request: 'http|https|ws|wss' or arbitrary value given by the headers. - `host`: The host associated with the request(which in the `Host` header): `localhost:8080` @@ -157,7 +157,8 @@ The following variables are accessible as properties on `Request` objects: - `url_for`: Just like `sanic.Sanic.url_for`, but automatically determine `scheme` and `netloc` base on the request. Since this method is aiming to generate correct schema & netloc, `_external` is implied. -## Changing the default parsing rules of the queryset +Changing the default parsing rules of the queryset +-------------------------------------------------- The default parameters that are using internally in `args` and `query_args` properties to parse queryset: @@ -177,32 +178,33 @@ with the new values. For the queryset `/?test1=value1&test2=&test3=value3`: -```python -from sanic.response import json +.. code-block:: python -@app.route("/query_string") -def query_string(request): - args_with_blank_values = request.get_args(keep_blank_values=True) - return json({ - "parsed": True, - "url": request.url, - "args_with_blank_values": args_with_blank_values, - "query_string": request.query_string - }) -``` + from sanic.response import json + + @app.route("/query_string") + def query_string(request): + args_with_blank_values = request.get_args(keep_blank_values=True) + return json({ + "parsed": True, + "url": request.url, + "args_with_blank_values": args_with_blank_values, + "query_string": request.query_string + }) The output will be: -``` -{ - "parsed": true, - "url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3", - "args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]}, - "query_string": "test1=value1&test2=&test3=value3" -} -``` +.. code-block:: JSON -## Accessing values using `get` and `getlist` + { + "parsed": true, + "url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3", + "args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]}, + "query_string": "test1=value1&test2=&test3=value3" + } + +Accessing values using `get` and `getlist` +------------------------------------------ The `request.args` returns a subclass of `dict` called `RequestParameters`. The key difference when using this object is the distinction between the `get` and `getlist` methods. @@ -211,16 +213,16 @@ The key difference when using this object is the distinction between the `get` a the given key is a list, *only the first item is returned*. - `getlist(key, default=None)` operates as normal, *returning the entire list*. -```python -from sanic.request import RequestParameters +.. code-block:: python -args = RequestParameters() -args['titles'] = ['Post 1', 'Post 2'] + from sanic.request import RequestParameters -args.get('titles') # => 'Post 1' + args = RequestParameters() + args['titles'] = ['Post 1', 'Post 2'] -args.getlist('titles') # => ['Post 1', 'Post 2'] -``` + args.get('titles') # => 'Post 1' + + args.getlist('titles') # => ['Post 1', 'Post 2'] ```python from sanic import Sanic @@ -240,34 +242,34 @@ def get_handler(request): The `request.endpoint` attribute holds the handler's name. For instance, the below route will return "hello". -```python -from sanic.response import text -from sanic import Sanic +.. code-block:: python -app = Sanic() + from sanic.response import text + from sanic import Sanic -@app.get("/") -def hello(request): - return text(request.endpoint) -``` + app = Sanic() + + @app.get("/") + def hello(request): + return text(request.endpoint) Or, with a blueprint it will be include both, separated by a period. For example, the below route would return foo.bar: -```python -from sanic import Sanic -from sanic import Blueprint -from sanic.response import text +.. code-block:: python + + from sanic import Sanic + from sanic import Blueprint + from sanic.response import text -app = Sanic(__name__) -blueprint = Blueprint('foo') + app = Sanic(__name__) + blueprint = Blueprint('foo') -@blueprint.get('/') -async def bar(request): - return text(request.endpoint) + @blueprint.get('/') + async def bar(request): + return text(request.endpoint) -app.blueprint(blueprint) + app.blueprint(blueprint) -app.run(host="0.0.0.0", port=8000, debug=True) -``` + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/docs/sanic/response.md b/docs/sanic/response.md deleted file mode 100644 index 11034727..00000000 --- a/docs/sanic/response.md +++ /dev/null @@ -1,114 +0,0 @@ -# Response - -Use functions in `sanic.response` module to create responses. - -## Plain Text - -```python -from sanic import response - - -@app.route('/text') -def handle_request(request): - return response.text('Hello world!') -``` - -## HTML - -```python -from sanic import response - - -@app.route('/html') -def handle_request(request): - return response.html('

Hello world!

') -``` - -## JSON - - -```python -from sanic import response - - -@app.route('/json') -def handle_request(request): - return response.json({'message': 'Hello world!'}) -``` - -## File - -```python -from sanic import response - - -@app.route('/file') -async def handle_request(request): - return await response.file('/srv/www/whatever.png') -``` - -## Streaming - -```python -from sanic import response - -@app.route("/streaming") -async def index(request): - async def streaming_fn(response): - await response.write('foo') - await response.write('bar') - return response.stream(streaming_fn, content_type='text/plain') -``` - -See [Streaming](streaming.md) for more information. - -## File Streaming -For large files, a combination of File and Streaming above -```python -from sanic import response - -@app.route('/big_file.png') -async def handle_request(request): - return await response.file_stream('/srv/www/whatever.png') -``` - -## Redirect - -```python -from sanic import response - - -@app.route('/redirect') -def handle_request(request): - return response.redirect('/json') -``` - -## Raw - -Response without encoding the body - -```python -from sanic import response - - -@app.route('/raw') -def handle_request(request): - return response.raw(b'raw data') -``` - -## Modify headers or status - -To modify headers or status code, pass the `headers` or `status` argument to those functions: - -```python -from sanic import response - - -@app.route('/json') -def handle_request(request): - return response.json( - {'message': 'Hello world!'}, - headers={'X-Served-By': 'sanic'}, - status=200 - ) -``` diff --git a/docs/sanic/response.rst b/docs/sanic/response.rst new file mode 100644 index 00000000..75a44425 --- /dev/null +++ b/docs/sanic/response.rst @@ -0,0 +1,126 @@ +Response +======== + +Use functions in `sanic.response` module to create responses. + +Plain Text +---------- + +.. code-block:: python + + from sanic import response + + + @app.route('/text') + def handle_request(request): + return response.text('Hello world!') + +HTML +---- + +.. code-block:: python + + from sanic import response + + + @app.route('/html') + def handle_request(request): + return response.html('

Hello world!

') + +JSON +---- + + +.. code-block:: python + + from sanic import response + + + @app.route('/json') + def handle_request(request): + return response.json({'message': 'Hello world!'}) + +File +---- + +.. code-block:: python + + from sanic import response + + + @app.route('/file') + async def handle_request(request): + return await response.file('/srv/www/whatever.png') + +Streaming +--------- + +.. code-block:: python + + from sanic import response + + @app.route("/streaming") + async def index(request): + async def streaming_fn(response): + await response.write('foo') + await response.write('bar') + return response.stream(streaming_fn, content_type='text/plain') + +See `Streaming `_ for more information. + +File Streaming +-------------- + +For large files, a combination of File and Streaming above + +.. code-block:: python + + from sanic import response + + @app.route('/big_file.png') + async def handle_request(request): + return await response.file_stream('/srv/www/whatever.png') + +Redirect +-------- + +.. code-block:: python + + from sanic import response + + + @app.route('/redirect') + def handle_request(request): + return response.redirect('/json') + +Raw +--- + +Response without encoding the body + +.. code-block:: python + + from sanic import response + + + @app.route('/raw') + def handle_request(request): + return response.raw(b'raw data') + +Modify headers or status +------------------------ + +To modify headers or status code, pass the `headers` or `status` argument to those functions: + +.. code-block:: python + + from sanic import response + + + @app.route('/json') + def handle_request(request): + return response.json( + {'message': 'Hello world!'}, + headers={'X-Served-By': 'sanic'}, + status=200 + ) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md deleted file mode 100644 index c1477aab..00000000 --- a/docs/sanic/routing.md +++ /dev/null @@ -1,429 +0,0 @@ -# Routing - -Routing allows the user to specify handler functions for different URL endpoints. - -A basic route looks like the following, where `app` is an instance of the -`Sanic` class: - -```python -from sanic.response import json - -@app.route("/") -async def test(request): - return json({ "hello": "world" }) -``` - -When the url `http://server.url/` is accessed (the base url of the server), the -final `/` is matched by the router to the handler function, `test`, which then -returns a JSON object. - -Sanic handler functions must be defined using the `async def` syntax, as they -are asynchronous functions. - -## Request parameters - -Sanic comes with a basic router that supports request parameters. - -To specify a parameter, surround it with angle quotes like so: ``. -Request parameters will be passed to the route handler functions as keyword -arguments. - -```python -from sanic.response import text - -@app.route('/tag/') -async def tag_handler(request, tag): - return text('Tag - {}'.format(tag)) -``` - -To specify a type for the parameter, add a `:type` after the parameter name, -inside the quotes. If the parameter does not match the specified type, Sanic -will throw a `NotFound` exception, resulting in a `404: Page not found` error -on the URL. - -### Supported types - -* `string` - * "Bob" - * "Python 3" -* `int` - * 10 - * 20 - * 30 - * -10 - * (No floats work here) -* `number` - * 1 - * 1.5 - * 10 - * -10 -* `alpha` - * "Bob" - * "Python" - * (If it contains a symbol or a non alphanumeric character it will fail) -* `path` - * "hello" - * "hello.text" - * "hello world" -* `uuid` - * 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support) -* `regex expression` - -If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type. - -```python -from sanic.response import text - -@app.route('/string/') -async def string_handler(request, string_arg): - return text('String - {}'.format(string_arg)) - -@app.route('/int/') -async def integer_handler(request, integer_arg): - return text('Integer - {}'.format(integer_arg)) - -@app.route('/number/') -async def number_handler(request, number_arg): - return text('Number - {}'.format(number_arg)) - -@app.route('/alpha/') -async def number_handler(request, alpha_arg): - return text('Alpha - {}'.format(alpha_arg)) - -@app.route('/path/') -async def number_handler(request, path_arg): - return text('Path - {}'.format(path_arg)) - -@app.route('/uuid/') -async def number_handler(request, uuid_arg): - return text('Uuid - {}'.format(uuid_arg)) - -@app.route('/person/') -async def person_handler(request, name): - return text('Person - {}'.format(name)) - -@app.route('/folder/') -async def folder_handler(request, folder_id): - return text('Folder - {}'.format(folder_id)) - -``` - -**Warning** `str` is not a valid type tag. If you want `str` recognition then you must use `string` - -## HTTP request types - -By default, a route defined on a URL will be available for only GET requests to that URL. -However, the `@app.route` decorator accepts an optional parameter, `methods`, -which allows the handler function to work with any of the HTTP methods in the list. - -```python -from sanic.response import text - -@app.route('/post', methods=['POST']) -async def post_handler(request): - return text('POST request - {}'.format(request.json)) - -@app.route('/get', methods=['GET']) -async def get_handler(request): - return text('GET request - {}'.format(request.args)) - -``` - -There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default. - -```python -@app.route('/get', methods=['GET'], host='example.com') -async def get_handler(request): - return text('GET request - {}'.format(request.args)) - -# if the host header doesn't match example.com, this route will be used -@app.route('/get', methods=['GET']) -async def get_handler(request): - return text('GET request in default - {}'.format(request.args)) -``` - -There are also shorthand method decorators: - -```python -from sanic.response import text - -@app.post('/post') -async def post_handler(request): - return text('POST request - {}'.format(request.json)) - -@app.get('/get') -async def get_handler(request): - return text('GET request - {}'.format(request.args)) - -``` -## The `add_route` method - -As we have seen, routes are often specified using the `@app.route` decorator. -However, this decorator is really just a wrapper for the `app.add_route` -method, which is used as follows: - -```python -from sanic.response import text - -# Define the handler functions -async def handler1(request): - return text('OK') - -async def handler2(request, name): - return text('Folder - {}'.format(name)) - -async def person_handler2(request, name): - return text('Person - {}'.format(name)) - -# Add each handler function as a route -app.add_route(handler1, '/test') -app.add_route(handler2, '/folder/') -app.add_route(person_handler2, '/person/', methods=['GET']) -``` - -## URL building with `url_for` - -Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example: - -```python -from sanic.response import redirect - -@app.route('/') -async def index(request): - # generate a URL for the endpoint `post_handler` - url = app.url_for('post_handler', post_id=5) - # the URL is `/posts/5`, redirect to it - return redirect(url) - -@app.route('/posts/') -async def post_handler(request, post_id): - return text('Post - {}'.format(post_id)) -``` - -Other things to keep in mind when using `url_for`: - -- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example: - -```python -url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two') -# /posts/5?arg_one=one&arg_two=two -``` - -- Multivalue argument can be passed to `url_for`. For example: - -```python -url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two']) -# /posts/5?arg_one=one&arg_one=two -``` - -- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example: - -```python -url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor') -# /posts/5?arg_one=one#anchor - -url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True) -# //server/posts/5?arg_one=one -# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external - -url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True) -# http://server/posts/5?arg_one=one -# when specifying _scheme, _external must be True - -# you can pass all special arguments at once -url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888') -# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor -``` - -- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be raised. - -## WebSocket routes - -Routes for the WebSocket protocol can be defined with the `@app.websocket` -decorator: - -```python -@app.websocket('/feed') -async def feed(request, ws): - while True: - data = 'hello!' - print('Sending: ' + data) - await ws.send(data) - data = await ws.recv() - print('Received: ' + data) -``` - -Alternatively, the `app.add_websocket_route` method can be used instead of the -decorator: - -```python -async def feed(request, ws): - pass - -app.add_websocket_route(my_websocket_handler, '/feed') -``` - -Handlers to a WebSocket route are invoked with the request as first argument, and a -WebSocket protocol object as second argument. The protocol object has `send` -and `recv` methods to send and receive data respectively. - -WebSocket support requires the [websockets](https://github.com/aaugustin/websockets) -package by Aymeric Augustin. - - -## About `strict_slashes` - -You can make `routes` strict to trailing slash or not, it's configurable. - -```python - -# provide default strict_slashes value for all routes -app = Sanic('test_route_strict_slash', strict_slashes=True) - -# you can also overwrite strict_slashes value for specific route -@app.get('/get', strict_slashes=False) -def handler(request): - return text('OK') - -# It also works for blueprints -bp = Blueprint('test_bp_strict_slash', strict_slashes=True) - -@bp.get('/bp/get', strict_slashes=False) -def handler(request): - return text('OK') - -app.blueprint(bp) -``` - -The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route -falls under the `strict_slashes` behavior. - -```bash -|___ Route - |___ Blueprint - |___ Application -``` - -Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes` -found in the above order will be applied to the route in question. - -```python -from sanic import Sanic, Blueprint -from sanic.response import text - -app = Sanic("sample_strict_slashes", strict_slashes=True) - -@app.get("/r1") -def r1(request): - return text("strict_slashes is applicable from App level") - -@app.get("/r2", strict_slashes=False) -def r2(request): - return text("strict_slashes is not applicable due to False value set in route level") - -bp = Blueprint("bp", strict_slashes=False) - -@bp.get("/r3", strict_slashes=True) -def r3(request): - return text("strict_slashes applicable from blueprint route level") - -bp1 = Blueprint("bp1", strict_slashes=True) - -@bp.get("/r4") -def r3(request): - return text("strict_slashes applicable from blueprint level") -``` - -## User defined route name - -A custom route name can be used by passing a `name` argument while registering the route which will -override the default route name generated using the `handler.__name__` attribute. - -```python - -app = Sanic('test_named_route') - -@app.get('/get', name='get_handler') -def handler(request): - return text('OK') - -# then you need use `app.url_for('get_handler')` -# instead of # `app.url_for('handler')` - -# It also works for blueprints -bp = Blueprint('test_named_bp') - -@bp.get('/bp/get', name='get_handler') -def handler(request): - return text('OK') - -app.blueprint(bp) - -# then you need use `app.url_for('test_named_bp.get_handler')` -# instead of `app.url_for('test_named_bp.handler')` - -# different names can be used for same url with different methods - -@app.get('/test', name='route_test') -def handler(request): - return text('OK') - -@app.post('/test', name='route_post') -def handler2(request): - return text('OK POST') - -@app.put('/test', name='route_put') -def handler3(request): - return text('OK PUT') - -# below url are the same, you can use any of them -# '/test' -app.url_for('route_test') -# app.url_for('route_post') -# app.url_for('route_put') - -# for same handler name with different methods -# you need specify the name (it's url_for issue) -@app.get('/get') -def handler(request): - return text('OK') - -@app.post('/post', name='post_handler') -def handler(request): - return text('OK') - -# then -# app.url_for('handler') == '/get' -# app.url_for('post_handler') == '/post' -``` - -## Build URL for static files - -Sanic supports using `url_for` method to build static file urls. In case if the static url -is pointing to a directory, `filename` parameter to the `url_for` can be ignored. q - -```python - -app = Sanic('test_static') -app.static('/static', './static') -app.static('/uploads', './uploads', name='uploads') -app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') - -bp = Blueprint('bp', url_prefix='bp') -bp.static('/static', './static') -bp.static('/uploads', './uploads', name='uploads') -bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') -app.blueprint(bp) - -# then build the url -app.url_for('static', filename='file.txt') == '/static/file.txt' -app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' -app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt' -app.url_for('static', name='best_png') == '/the_best.png' - -# blueprint url building -app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' -app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt' -app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png' - -``` diff --git a/docs/sanic/routing.rst b/docs/sanic/routing.rst new file mode 100644 index 00000000..b073ed50 --- /dev/null +++ b/docs/sanic/routing.rst @@ -0,0 +1,433 @@ +Routing +------- + +Routing allows the user to specify handler functions for different URL endpoints. + +A basic route looks like the following, where `app` is an instance of the +`Sanic` class: + +.. code-block:: python + + from sanic.response import json + + @app.route("/") + async def test(request): + return json({ "hello": "world" }) + +When the url `http://server.url/` is accessed (the base url of the server), the +final `/` is matched by the router to the handler function, `test`, which then +returns a JSON object. + +Sanic handler functions must be defined using the `async def` syntax, as they +are asynchronous functions. + +Request parameters +================== + +Sanic comes with a basic router that supports request parameters. + +To specify a parameter, surround it with angle quotes like so: ``. +Request parameters will be passed to the route handler functions as keyword +arguments. + +.. code-block:: python + + from sanic.response import text + + @app.route('/tag/') + async def tag_handler(request, tag): + return text('Tag - {}'.format(tag)) + +To specify a type for the parameter, add a `:type` after the parameter name, +inside the quotes. If the parameter does not match the specified type, Sanic +will throw a `NotFound` exception, resulting in a `404: Page not found` error +on the URL. + +Supported types +~~~~~~~~~~~~~~~ + +* `string` + * "Bob" + * "Python 3" +* `int` + * 10 + * 20 + * 30 + * -10 + * (No floats work here) +* `number` + * 1 + * 1.5 + * 10 + * -10 +* `alpha` + * "Bob" + * "Python" + * (If it contains a symbol or a non alphanumeric character it will fail) +* `path` + * "hello" + * "hello.text" + * "hello world" +* `uuid` + * 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support) +* `regex expression` + +If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type. + +.. code-block:: python + + from sanic.response import text + + @app.route('/string/') + async def string_handler(request, string_arg): + return text('String - {}'.format(string_arg)) + + @app.route('/int/') + async def integer_handler(request, integer_arg): + return text('Integer - {}'.format(integer_arg)) + + @app.route('/number/') + async def number_handler(request, number_arg): + return text('Number - {}'.format(number_arg)) + + @app.route('/alpha/') + async def number_handler(request, alpha_arg): + return text('Alpha - {}'.format(alpha_arg)) + + @app.route('/path/') + async def number_handler(request, path_arg): + return text('Path - {}'.format(path_arg)) + + @app.route('/uuid/') + async def number_handler(request, uuid_arg): + return text('Uuid - {}'.format(uuid_arg)) + + @app.route('/person/') + async def person_handler(request, name): + return text('Person - {}'.format(name)) + + @app.route('/folder/') + async def folder_handler(request, folder_id): + return text('Folder - {}'.format(folder_id)) + +.. warning:: + + `str` is not a valid type tag. If you want `str` recognition then you must use `string` + +HTTP request types +================== + +By default, a route defined on a URL will be available for only GET requests to that URL. +However, the `@app.route` decorator accepts an optional parameter, `methods`, +which allows the handler function to work with any of the HTTP methods in the list. + +.. code-block:: python + + from sanic.response import text + + @app.route('/post', methods=['POST']) + async def post_handler(request): + return text('POST request - {}'.format(request.json)) + + @app.route('/get', methods=['GET']) + async def get_handler(request): + return text('GET request - {}'.format(request.args)) + +There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default. + +.. code-block:: python + + @app.route('/get', methods=['GET'], host='example.com') + async def get_handler(request): + return text('GET request - {}'.format(request.args)) + + # if the host header doesn't match example.com, this route will be used + @app.route('/get', methods=['GET']) + async def get_handler(request): + return text('GET request in default - {}'.format(request.args)) + +There are also shorthand method decorators: + +.. code-block:: python + + from sanic.response import text + + @app.post('/post') + async def post_handler(request): + return text('POST request - {}'.format(request.json)) + + @app.get('/get') + async def get_handler(request): + return text('GET request - {}'.format(request.args)) + +The `add_route` method +====================== + +As we have seen, routes are often specified using the `@app.route` decorator. +However, this decorator is really just a wrapper for the `app.add_route` +method, which is used as follows: + +.. code-block:: python + + from sanic.response import text + + # Define the handler functions + async def handler1(request): + return text('OK') + + async def handler2(request, name): + return text('Folder - {}'.format(name)) + + async def person_handler2(request, name): + return text('Person - {}'.format(name)) + + # Add each handler function as a route + app.add_route(handler1, '/test') + app.add_route(handler2, '/folder/') + app.add_route(person_handler2, '/person/', methods=['GET']) + +URL building with `url_for` +=========================== + +Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example: + +.. code-block:: python + + from sanic.response import redirect + + @app.route('/') + async def index(request): + # generate a URL for the endpoint `post_handler` + url = app.url_for('post_handler', post_id=5) + # the URL is `/posts/5`, redirect to it + return redirect(url) + + @app.route('/posts/') + async def post_handler(request, post_id): + return text('Post - {}'.format(post_id)) + +Other things to keep in mind when using `url_for`: + +- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example: + +.. code-block:: python + + url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two') + # /posts/5?arg_one=one&arg_two=two + +- Multivalue argument can be passed to `url_for`. For example: + +.. code-block:: python + + url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two']) + # /posts/5?arg_one=one&arg_one=two + +- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example: + +.. code-block:: python + + url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor') + # /posts/5?arg_one=one#anchor + + url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True) + # //server/posts/5?arg_one=one + # _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external + + url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True) + # http://server/posts/5?arg_one=one + # when specifying _scheme, _external must be True + + # you can pass all special arguments at once + url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888') + # http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor + +- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be raised. + +WebSocket routes +================ + +Routes for the WebSocket protocol can be defined with the `@app.websocket` +decorator: + +.. code-block:: python + + @app.websocket('/feed') + async def feed(request, ws): + while True: + data = 'hello!' + print('Sending: ' + data) + await ws.send(data) + data = await ws.recv() + print('Received: ' + data) + +Alternatively, the `app.add_websocket_route` method can be used instead of the +decorator: + +.. code-block:: python + + async def feed(request, ws): + pass + + app.add_websocket_route(my_websocket_handler, '/feed') + +Handlers to a WebSocket route are invoked with the request as first argument, and a +WebSocket protocol object as second argument. The protocol object has `send` +and `recv` methods to send and receive data respectively. + +WebSocket support requires the `websockets `_ +package by Aymeric Augustin. + + +About `strict_slashes` +====================== + +You can make `routes` strict to trailing slash or not, it's configurable. + +.. code-block:: python + + # provide default strict_slashes value for all routes + app = Sanic('test_route_strict_slash', strict_slashes=True) + + # you can also overwrite strict_slashes value for specific route + @app.get('/get', strict_slashes=False) + def handler(request): + return text('OK') + + # It also works for blueprints + bp = Blueprint('test_bp_strict_slash', strict_slashes=True) + + @bp.get('/bp/get', strict_slashes=False) + def handler(request): + return text('OK') + + app.blueprint(bp) + +The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route +falls under the `strict_slashes` behavior. + +| Route/ +| β”œβ”€β”€Blueprint/ +| β”œβ”€β”€Application/ + +Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes` +found in the above order will be applied to the route in question. + +.. code-block:: python + + from sanic import Sanic, Blueprint + from sanic.response import text + + app = Sanic("sample_strict_slashes", strict_slashes=True) + + @app.get("/r1") + def r1(request): + return text("strict_slashes is applicable from App level") + + @app.get("/r2", strict_slashes=False) + def r2(request): + return text("strict_slashes is not applicable due to False value set in route level") + + bp = Blueprint("bp", strict_slashes=False) + + @bp.get("/r3", strict_slashes=True) + def r3(request): + return text("strict_slashes applicable from blueprint route level") + + bp1 = Blueprint("bp1", strict_slashes=True) + + @bp.get("/r4") + def r3(request): + return text("strict_slashes applicable from blueprint level") + +User defined route name +======================= + +A custom route name can be used by passing a `name` argument while registering the route which will +override the default route name generated using the `handler.__name__` attribute. + +.. code-block:: python + + app = Sanic('test_named_route') + + @app.get('/get', name='get_handler') + def handler(request): + return text('OK') + + # then you need use `app.url_for('get_handler')` + # instead of # `app.url_for('handler')` + + # It also works for blueprints + bp = Blueprint('test_named_bp') + + @bp.get('/bp/get', name='get_handler') + def handler(request): + return text('OK') + + app.blueprint(bp) + + # then you need use `app.url_for('test_named_bp.get_handler')` + # instead of `app.url_for('test_named_bp.handler')` + + # different names can be used for same url with different methods + + @app.get('/test', name='route_test') + def handler(request): + return text('OK') + + @app.post('/test', name='route_post') + def handler2(request): + return text('OK POST') + + @app.put('/test', name='route_put') + def handler3(request): + return text('OK PUT') + + # below url are the same, you can use any of them + # '/test' + app.url_for('route_test') + # app.url_for('route_post') + # app.url_for('route_put') + + # for same handler name with different methods + # you need specify the name (it's url_for issue) + @app.get('/get') + def handler(request): + return text('OK') + + @app.post('/post', name='post_handler') + def handler(request): + return text('OK') + + # then + # app.url_for('handler') == '/get' + # app.url_for('post_handler') == '/post' + +Build URL for static files +========================== + +Sanic supports using `url_for` method to build static file urls. In case if the static url +is pointing to a directory, `filename` parameter to the `url_for` can be ignored. q + +.. code-block:: python + + app = Sanic('test_static') + app.static('/static', './static') + app.static('/uploads', './uploads', name='uploads') + app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + + bp = Blueprint('bp', url_prefix='bp') + bp.static('/static', './static') + bp.static('/uploads', './uploads', name='uploads') + bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + app.blueprint(bp) + + # then build the url + app.url_for('static', filename='file.txt') == '/static/file.txt' + app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' + app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt' + app.url_for('static', name='best_png') == '/the_best.png' + + # blueprint url building + app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' + app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt' + app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png' diff --git a/docs/sanic/static_files.md b/docs/sanic/static_files.md deleted file mode 100644 index 601f359c..00000000 --- a/docs/sanic/static_files.md +++ /dev/null @@ -1,87 +0,0 @@ -# Static Files - -Static files and directories, such as an image file, are served by Sanic when -registered with the `app.static()` method. The method takes an endpoint URL and a -filename. The file specified will then be accessible via the given endpoint. - -```python -from sanic import Sanic -from sanic.blueprints import Blueprint - -app = Sanic(__name__) - -# Serves files from the static folder to the URL /static -app.static('/static', './static') -# use url_for to build the url, name defaults to 'static' and can be ignored -app.url_for('static', filename='file.txt') == '/static/file.txt' -app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' - -# Serves the file /home/ubuntu/test.png when the URL /the_best.png -# is requested -app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') - -# you can use url_for to build the static file url -# you can ignore name and filename parameters if you don't define it -app.url_for('static', name='best_png') == '/the_best.png' -app.url_for('static', name='best_png', filename='any') == '/the_best.png' - -# you need define the name for other static files -app.static('/another.png', '/home/ubuntu/another.png', name='another') -app.url_for('static', name='another') == '/another.png' -app.url_for('static', name='another', filename='any') == '/another.png' - -# also, you can use static for blueprint -bp = Blueprint('bp', url_prefix='/bp') -bp.static('/static', './static') - -# specify a different content_type for your files -# such as adding 'charset' -app.static('/', '/public/index.html', content_type="text/html; charset=utf-8") - -# servers the file directly -bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') -app.blueprint(bp) - -app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' -app.url_for('static', name='bp.best_png') == '/bp/test_best.png' - -app.run(host="0.0.0.0", port=8000) -``` - -> **Note:** Sanic does not provide directory index when you serve a static directory. - -## Virtual Host - -The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example: - -```python -from sanic import Sanic - -app = Sanic(__name__) - -app.static('/static', './static') -app.static('/example_static', './example_static', host='www.example.com') -``` - -## Streaming Large File - -In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly. - -Here is an example: -```python -from sanic import Sanic - -app = Sanic(__name__) - -app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True) -``` - -When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example: -```python -from sanic import Sanic - -app = Sanic(__name__) - -chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB -app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size) -``` diff --git a/docs/sanic/static_files.rst b/docs/sanic/static_files.rst new file mode 100644 index 00000000..1c93a682 --- /dev/null +++ b/docs/sanic/static_files.rst @@ -0,0 +1,92 @@ +Static Files +============ + +Static files and directories, such as an image file, are served by Sanic when +registered with the `app.static()` method. The method takes an endpoint URL and a +filename. The file specified will then be accessible via the given endpoint. + +.. code-block:: python + + from sanic import Sanic + from sanic.blueprints import Blueprint + + app = Sanic(__name__) + + # Serves files from the static folder to the URL /static + app.static('/static', './static') + # use url_for to build the url, name defaults to 'static' and can be ignored + app.url_for('static', filename='file.txt') == '/static/file.txt' + app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' + + # Serves the file /home/ubuntu/test.png when the URL /the_best.png + # is requested + app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + + # you can use url_for to build the static file url + # you can ignore name and filename parameters if you don't define it + app.url_for('static', name='best_png') == '/the_best.png' + app.url_for('static', name='best_png', filename='any') == '/the_best.png' + + # you need define the name for other static files + app.static('/another.png', '/home/ubuntu/another.png', name='another') + app.url_for('static', name='another') == '/another.png' + app.url_for('static', name='another', filename='any') == '/another.png' + + # also, you can use static for blueprint + bp = Blueprint('bp', url_prefix='/bp') + bp.static('/static', './static') + + # specify a different content_type for your files + # such as adding 'charset' + app.static('/', '/public/index.html', content_type="text/html; charset=utf-8") + + # servers the file directly + bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + app.blueprint(bp) + + app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' + app.url_for('static', name='bp.best_png') == '/bp/test_best.png' + + app.run(host="0.0.0.0", port=8000) + +> **Note:** Sanic does not provide directory index when you serve a static directory. + +Virtual Host +------------ + +The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example: + +.. code-block:: python + + from sanic import Sanic + + app = Sanic(__name__) + + app.static('/static', './static') + app.static('/example_static', './example_static', host='www.example.com') + +Streaming Large File +-------------------- + +In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly. + +Here is an example: + +.. code-block:: python + + from sanic import Sanic + + app = Sanic(__name__) + + app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True) + +When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example: + +.. code-block:: python + + from sanic import Sanic + + app = Sanic(__name__) + + chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB + app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size) diff --git a/docs/sanic/streaming.md b/docs/sanic/streaming.md deleted file mode 100644 index 29e28bf7..00000000 --- a/docs/sanic/streaming.md +++ /dev/null @@ -1,143 +0,0 @@ -# Streaming - -## Request Streaming - -Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument. - -```python -from sanic import Sanic -from sanic.views import CompositionView -from sanic.views import HTTPMethodView -from sanic.views import stream as stream_decorator -from sanic.blueprints import Blueprint -from sanic.response import stream, text - -bp = Blueprint('blueprint_request_stream') -app = Sanic('request_stream') - - -class SimpleView(HTTPMethodView): - - @stream_decorator - async def post(self, request): - result = '' - while True: - body = await request.stream.read() - if body is None: - break - result += body.decode('utf-8') - return text(result) - - -@app.post('/stream', stream=True) -async def handler(request): - async def streaming(response): - while True: - body = await request.stream.read() - if body is None: - break - body = body.decode('utf-8').replace('1', 'A') - await response.write(body) - return stream(streaming) - - -@bp.put('/bp_stream', stream=True) -async def bp_put_handler(request): - result = '' - while True: - body = await request.stream.read() - if body is None: - break - result += body.decode('utf-8').replace('1', 'A') - return text(result) - - -# You can also use `bp.add_route()` with stream argument -async def bp_post_handler(request): - result = '' - while True: - body = await request.stream.read() - if body is None: - break - result += body.decode('utf-8').replace('1', 'A') - return text(result) - -bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True) - - -async def post_handler(request): - result = '' - while True: - body = await request.stream.read() - if body is None: - break - result += body.decode('utf-8') - return text(result) - -app.blueprint(bp) -app.add_route(SimpleView.as_view(), '/method_view') -view = CompositionView() -view.add(['POST'], post_handler, stream=True) -app.add_route(view, '/composition_view') - - -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8000) -``` - -## Response Streaming - -Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows: - -```python -from sanic import Sanic -from sanic.response import stream - -app = Sanic(__name__) - -@app.route("/") -async def test(request): - async def sample_streaming_fn(response): - await response.write('foo,') - await response.write('bar') - - return stream(sample_streaming_fn, content_type='text/csv') -``` - -This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides: - -```python -@app.route("/") -async def index(request): - async def stream_from_db(response): - conn = await asyncpg.connect(database='test') - async with conn.transaction(): - async for record in conn.cursor('SELECT generate_series(0, 10)'): - await response.write(record[0]) - - return stream(stream_from_db) -``` - -If a client supports HTTP/1.1, Sanic will use [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding); you can explicitly enable or disable it using `chunked` option of the `stream` function. - -## File Streaming - -Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually: - -```python -from aiofiles import os as async_os -from sanic.response import file_stream - -@app.route("/") -async def index(request): - file_path = "/srv/www/whatever.png" - - file_stat = await async_os.stat(file_path) - headers = {"Content-Length": str(file_stat.st_size)} - - return await file_stream( - file_path, - headers=headers, - chunked=False, - ) -``` diff --git a/docs/sanic/streaming.rst b/docs/sanic/streaming.rst new file mode 100644 index 00000000..681a303d --- /dev/null +++ b/docs/sanic/streaming.rst @@ -0,0 +1,147 @@ +Streaming +========= + +Request Streaming +----------------- + +Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument. + +.. code-block:: python + + from sanic import Sanic + from sanic.views import CompositionView + from sanic.views import HTTPMethodView + from sanic.views import stream as stream_decorator + from sanic.blueprints import Blueprint + from sanic.response import stream, text + + bp = Blueprint('blueprint_request_stream') + app = Sanic('request_stream') + + + class SimpleView(HTTPMethodView): + + @stream_decorator + async def post(self, request): + result = '' + while True: + body = await request.stream.read() + if body is None: + break + result += body.decode('utf-8') + return text(result) + + + @app.post('/stream', stream=True) + async def handler(request): + async def streaming(response): + while True: + body = await request.stream.read() + if body is None: + break + body = body.decode('utf-8').replace('1', 'A') + await response.write(body) + return stream(streaming) + + + @bp.put('/bp_stream', stream=True) + async def bp_put_handler(request): + result = '' + while True: + body = await request.stream.read() + if body is None: + break + result += body.decode('utf-8').replace('1', 'A') + return text(result) + + + # You can also use `bp.add_route()` with stream argument + async def bp_post_handler(request): + result = '' + while True: + body = await request.stream.read() + if body is None: + break + result += body.decode('utf-8').replace('1', 'A') + return text(result) + + bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True) + + + async def post_handler(request): + result = '' + while True: + body = await request.stream.read() + if body is None: + break + result += body.decode('utf-8') + return text(result) + + app.blueprint(bp) + app.add_route(SimpleView.as_view(), '/method_view') + view = CompositionView() + view.add(['POST'], post_handler, stream=True) + app.add_route(view, '/composition_view') + + + if __name__ == '__main__': + app.run(host='127.0.0.1', port=8000) + +Response Streaming +------------------ + +Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows: + +.. code-block:: python + + from sanic import Sanic + from sanic.response import stream + + app = Sanic(__name__) + + @app.route("/") + async def test(request): + async def sample_streaming_fn(response): + await response.write('foo,') + await response.write('bar') + + return stream(sample_streaming_fn, content_type='text/csv') + +This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides: + +.. code-block:: python + + @app.route("/") + async def index(request): + async def stream_from_db(response): + conn = await asyncpg.connect(database='test') + async with conn.transaction(): + async for record in conn.cursor('SELECT generate_series(0, 10)'): + await response.write(record[0]) + + return stream(stream_from_db) + +If a client supports HTTP/1.1, Sanic will use `chunked transfer encoding `_; you can explicitly enable or disable it using `chunked` option of the `stream` function. + +File Streaming +-------------- + +Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually: + +.. code-block:: python + + from aiofiles import os as async_os + from sanic.response import file_stream + + @app.route("/") + async def index(request): + file_path = "/srv/www/whatever.png" + + file_stat = await async_os.stat(file_path) + headers = {"Content-Length": str(file_stat.st_size)} + + return await file_stream( + file_path, + headers=headers, + chunked=False, + ) diff --git a/docs/sanic/testing.md b/docs/sanic/testing.md deleted file mode 100644 index 64cdef4f..00000000 --- a/docs/sanic/testing.md +++ /dev/null @@ -1,144 +0,0 @@ -# Testing - -Sanic endpoints can be tested locally using the `test_client` object, which -depends on the additional [`requests-async`](https://github.com/encode/requests-async) -library, which implements an API that mirrors the `requests` library. - -The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods -for you to run against your application. A simple example (using pytest) is like follows: - -```python -# Import the Sanic app, usually created with Sanic(__name__) -from external_server import app - -def test_index_returns_200(): - request, response = app.test_client.get('/') - assert response.status == 200 - -def test_index_put_not_allowed(): - request, response = app.test_client.put('/') - assert response.status == 405 -``` - -Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and -your test request is executed against your application, using `requests-async`. - -The `test_client` methods accept the following arguments and keyword arguments: - -- `uri` *(default `'/'`)* A string representing the URI to test. -- `gather_request` *(default `True`)* A boolean which determines whether the - original request will be returned by the function. If set to `True`, the - return value is a tuple of `(request, response)`, if `False` only the - response is returned. -- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run. -- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode. - -The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request. - -For example, to supply data to a GET request, you would do the following: - -```python -def test_get_request_includes_data(): - params = {'key1': 'value1', 'key2': 'value2'} - request, response = app.test_client.get('/', params=params) - assert request.args.get('key1') == 'value1' -``` - -And to supply data to a JSON POST request: - -```python -def test_post_json_request_includes_data(): - data = {'key1': 'value1', 'key2': 'value2'} - request, response = app.test_client.post('/', data=json.dumps(data)) - assert request.json.get('key1') == 'value1' -``` - - -More information about -the available arguments to `requests-async` can be found -[in the documentation for `requests`](https://2.python-requests.org/en/master/). - - -## Using a random port - -If you need to test using a free unpriveleged port chosen by the kernel -instead of the default with `SanicTestClient`, you can do so by specifying -`port=None`. On most systems the port will be in the range 1024 to 65535. - -```python -# Import the Sanic app, usually created with Sanic(__name__) -from external_server import app -from sanic.testing import SanicTestClient - -def test_index_returns_200(): - request, response = SanicTestClient(app, port=None).get('/') - assert response.status == 200 -``` - - -## pytest-sanic - -[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously. -Just write tests like, - -```python -async def test_sanic_db_find_by_id(app): - """ - Let's assume that, in db we have, - { - "id": "123", - "name": "Kobe Bryant", - "team": "Lakers", - } - """ - doc = await app.db["players"].find_by_id("123") - assert doc.name == "Kobe Bryant" - assert doc.team == "Lakers" -``` - -[pytest-sanic](https://github.com/yunstanford/pytest-sanic) also provides some useful fixtures, like loop, unused_port, -test_server, test_client. - -```python -@pytest.yield_fixture -def app(): - app = Sanic("test_sanic_app") - - @app.route("/test_get", methods=['GET']) - async def test_get(request): - return response.json({"GET": True}) - - @app.route("/test_post", methods=['POST']) - async def test_post(request): - return response.json({"POST": True}) - - yield app - - -@pytest.fixture -def test_cli(loop, app, test_client): - return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol)) - - -######### -# Tests # -######### - -async def test_fixture_test_client_get(test_cli): - """ - GET request - """ - resp = await test_cli.get('/test_get') - assert resp.status == 200 - resp_json = await resp.json() - assert resp_json == {"GET": True} - -async def test_fixture_test_client_post(test_cli): - """ - POST request - """ - resp = await test_cli.post('/test_post') - assert resp.status == 200 - resp_json = await resp.json() - assert resp_json == {"POST": True} -``` diff --git a/docs/sanic/testing.rst b/docs/sanic/testing.rst new file mode 100644 index 00000000..5dba2ab4 --- /dev/null +++ b/docs/sanic/testing.rst @@ -0,0 +1,145 @@ +Testing +======= + +Sanic endpoints can be tested locally using the `test_client` object, which +depends on the additional `requests-async `_ +library, which implements an API that mirrors the `requests` library. + +The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods +for you to run against your application. A simple example (using pytest) is like follows: + +.. code-block:: python + + # Import the Sanic app, usually created with Sanic(__name__) + from external_server import app + + def test_index_returns_200(): + request, response = app.test_client.get('/') + assert response.status == 200 + + def test_index_put_not_allowed(): + request, response = app.test_client.put('/') + assert response.status == 405 + +Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and +your test request is executed against your application, using `requests-async`. + +The `test_client` methods accept the following arguments and keyword arguments: + +- `uri` *(default `'/'`)* A string representing the URI to test. +- `gather_request` *(default `True`)* A boolean which determines whether the + original request will be returned by the function. If set to `True`, the + return value is a tuple of `(request, response)`, if `False` only the + response is returned. +- `server_kwargs` *(default `{}`)* a dict of additional arguments to pass into `app.run` before the test request is run. +- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode. + +The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request. + +For example, to supply data to a GET request, you would do the following: + +.. code-block:: python + + def test_get_request_includes_data(): + params = {'key1': 'value1', 'key2': 'value2'} + request, response = app.test_client.get('/', params=params) + assert request.args.get('key1') == 'value1' + +And to supply data to a JSON POST request: + +.. code-block:: python + + def test_post_json_request_includes_data(): + data = {'key1': 'value1', 'key2': 'value2'} + request, response = app.test_client.post('/', data=json.dumps(data)) + assert request.json.get('key1') == 'value1' + +More information about +the available arguments to `requests-async` can be found +[in the documentation for `requests `_. + + +Using a random port +------------------- + +If you need to test using a free unpriveleged port chosen by the kernel +instead of the default with `SanicTestClient`, you can do so by specifying +`port=None`. On most systems the port will be in the range 1024 to 65535. + +.. code-block:: python + + # Import the Sanic app, usually created with Sanic(__name__) + from external_server import app + from sanic.testing import SanicTestClient + + def test_index_returns_200(): + request, response = SanicTestClient(app, port=None).get('/') + assert response.status == 200 + +pytest-sanic +------------ + +`pytest-sanic `_ is a pytest plugin, it helps you to test your code asynchronously. +Just write tests like, + +.. code-block:: python + + async def test_sanic_db_find_by_id(app): + """ + Let's assume that, in db we have, + { + "id": "123", + "name": "Kobe Bryant", + "team": "Lakers", + } + """ + doc = await app.db["players"].find_by_id("123") + assert doc.name == "Kobe Bryant" + assert doc.team == "Lakers" + +`pytest-sanic `_ also provides some useful fixtures, like loop, unused_port, +test_server, test_client. + +.. code-block:: python + + @pytest.yield_fixture + def app(): + app = Sanic("test_sanic_app") + + @app.route("/test_get", methods=['GET']) + async def test_get(request): + return response.json({"GET": True}) + + @app.route("/test_post", methods=['POST']) + async def test_post(request): + return response.json({"POST": True}) + + yield app + + + @pytest.fixture + def test_cli(loop, app, test_client): + return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol)) + + + ######### + # Tests # + ######### + + async def test_fixture_test_client_get(test_cli): + """ + GET request + """ + resp = await test_cli.get('/test_get') + assert resp.status == 200 + resp_json = await resp.json() + assert resp_json == {"GET": True} + + async def test_fixture_test_client_post(test_cli): + """ + POST request + """ + resp = await test_cli.post('/test_post') + assert resp.status == 200 + resp_json = await resp.json() + assert resp_json == {"POST": True} diff --git a/docs/sanic/versioning.md b/docs/sanic/versioning.md deleted file mode 100644 index ab6dab22..00000000 --- a/docs/sanic/versioning.md +++ /dev/null @@ -1,50 +0,0 @@ -# Versioning - -You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number. - -## Per route - -You can pass a version number to the routes directly. - -```python -from sanic import response - - -@app.route('/text', version=1) -def handle_request(request): - return response.text('Hello world! Version 1') - -@app.route('/text', version=2) -def handle_request(request): - return response.text('Hello world! Version 2') - -app.run(port=80) -``` - -Then with curl: - -```bash -curl localhost/v1/text -curl localhost/v2/text -``` - -## Global blueprint version - -You can also pass a version number to the blueprint, which will apply to all routes. - -```python -from sanic import response -from sanic.blueprints import Blueprint - -bp = Blueprint('test', version=1) - -@bp.route('/html') -def handle_request(request): - return response.html('

Hello world!

') -``` - -Then with curl: - -```bash -curl localhost/v1/html -``` diff --git a/docs/sanic/versioning.rst b/docs/sanic/versioning.rst new file mode 100644 index 00000000..0d748a78 --- /dev/null +++ b/docs/sanic/versioning.rst @@ -0,0 +1,54 @@ +Versioning +========== + +You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number. + +Per route +--------- + +You can pass a version number to the routes directly. + +.. code-block:: python + + from sanic import response + + + @app.route('/text', version=1) + def handle_request(request): + return response.text('Hello world! Version 1') + + @app.route('/text', version=2) + def handle_request(request): + return response.text('Hello world! Version 2') + + app.run(port=80) + +Then with curl: + +.. code-block:: bash + + curl localhost/v1/text + curl localhost/v2/text + + +Global blueprint version +------------------------ + +You can also pass a version number to the blueprint, which will apply to all routes. + +.. code-block:: python + + from sanic import response + from sanic.blueprints import Blueprint + + bp = Blueprint('test', version=1) + + @bp.route('/html') + def handle_request(request): + return response.html('

Hello world!

') + +Then with curl: + +.. code-block:: bash + + curl localhost/v1/html From ed1f367a8a7cd47d3f647d684b13dc7708789afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Dantas?= Date: Thu, 14 Nov 2019 17:57:41 -0300 Subject: [PATCH 16/19] Reduce nesting for the sample authentication decorator (#1715) * Reduce nesting for the sample authentication decorator * Add missing decorator argument --- examples/authorized_sanic.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/examples/authorized_sanic.py b/examples/authorized_sanic.py index f6b17426..7b5b7501 100644 --- a/examples/authorized_sanic.py +++ b/examples/authorized_sanic.py @@ -13,28 +13,26 @@ def check_request_for_authorization_status(request): return flag -def authorized(): - def decorator(f): - @wraps(f) - async def decorated_function(request, *args, **kwargs): - # run some method that checks the request - # for the client's authorization status - is_authorized = check_request_for_authorization_status(request) +def authorized(f): + @wraps(f) + async def decorated_function(request, *args, **kwargs): + # run some method that checks the request + # for the client's authorization status + is_authorized = check_request_for_authorization_status(request) - if is_authorized: - # the user is authorized. - # run the handler method and return the response - response = await f(request, *args, **kwargs) - return response - else: - # the user is not authorized. - return json({'status': 'not_authorized'}, 403) - return decorated_function - return decorator + if is_authorized: + # the user is authorized. + # run the handler method and return the response + response = await f(request, *args, **kwargs) + return response + else: + # the user is not authorized. + return json({'status': 'not_authorized'}, 403) + return decorated_function @app.route("/") -@authorized() +@authorized async def test(request): return json({'status': 'authorized'}) From ecbe5c839f8a67775255c0b94c81cfba441ffdd1 Mon Sep 17 00:00:00 2001 From: Junyeong Jeong Date: Fri, 22 Nov 2019 00:33:50 +0900 Subject: [PATCH 17/19] pass request_buffer_queue_size argument to HttpProtocol (#1717) * pass request_buffer_queue_size argument to HttpProtocol * fix to use simultaneously only one task to put body to stream buffer * add a test code for REQUEST_BUFFER_QUEUE_SIZE --- sanic/request.py | 4 +++ sanic/server.py | 45 ++++++++++++++++++------- tests/test_request_buffer_queue_size.py | 36 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 tests/test_request_buffer_queue_size.py diff --git a/sanic/request.py b/sanic/request.py index 9712aeb3..246eb351 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -62,6 +62,10 @@ class StreamBuffer: def is_full(self): return self._queue.full() + @property + def buffer_size(self): + return self._queue.maxsize + class Request: """Properties of an HTTP request such as URL, headers, etc.""" diff --git a/sanic/server.py b/sanic/server.py index fa38e435..41af81c0 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -2,6 +2,7 @@ import asyncio import os import traceback +from collections import deque from functools import partial from inspect import isawaitable from multiprocessing import Process @@ -148,6 +149,7 @@ class HttpProtocol(asyncio.Protocol): self.state["requests_count"] = 0 self._debug = debug self._not_paused.set() + self._body_chunks = deque() @property def keep_alive(self): @@ -347,19 +349,30 @@ class HttpProtocol(asyncio.Protocol): def on_body(self, body): if self.is_request_stream and self._is_stream_handler: - self._request_stream_task = self.loop.create_task( - self.body_append(body) - ) + # body chunks can be put into asyncio.Queue out of order if + # multiple tasks put concurrently and the queue is full in python + # 3.7. so we should not create more than one task putting into the + # queue simultaneously. + self._body_chunks.append(body) + if ( + not self._request_stream_task + or self._request_stream_task.done() + ): + self._request_stream_task = self.loop.create_task( + self.stream_append() + ) else: self.request.body_push(body) - async def body_append(self, body): - if self.request.stream.is_full(): - self.transport.pause_reading() - await self.request.stream.put(body) - self.transport.resume_reading() - else: - await self.request.stream.put(body) + async def stream_append(self): + while self._body_chunks: + body = self._body_chunks.popleft() + if self.request.stream.is_full(): + self.transport.pause_reading() + await self.request.stream.put(body) + self.transport.resume_reading() + else: + await self.request.stream.put(body) def on_message_complete(self): # Entire request (headers and whole body) is received. @@ -368,9 +381,14 @@ class HttpProtocol(asyncio.Protocol): self._request_timeout_handler.cancel() self._request_timeout_handler = None if self.is_request_stream and self._is_stream_handler: - self._request_stream_task = self.loop.create_task( - self.request.stream.put(None) - ) + self._body_chunks.append(None) + if ( + not self._request_stream_task + or self._request_stream_task.done() + ): + self._request_stream_task = self.loop.create_task( + self.stream_append() + ) return self.request.body_finish() self.execute_request_handler() @@ -818,6 +836,7 @@ def serve( response_timeout=response_timeout, keep_alive_timeout=keep_alive_timeout, request_max_size=request_max_size, + request_buffer_queue_size=request_buffer_queue_size, request_class=request_class, access_log=access_log, keep_alive=keep_alive, diff --git a/tests/test_request_buffer_queue_size.py b/tests/test_request_buffer_queue_size.py new file mode 100644 index 00000000..1a9cfdf3 --- /dev/null +++ b/tests/test_request_buffer_queue_size.py @@ -0,0 +1,36 @@ +import io + +from sanic.response import text + +data = "abc" * 10_000_000 + + +def test_request_buffer_queue_size(app): + default_buf_qsz = app.config.get("REQUEST_BUFFER_QUEUE_SIZE") + qsz = 1 + while qsz == default_buf_qsz: + qsz += 1 + app.config.REQUEST_BUFFER_QUEUE_SIZE = qsz + + @app.post("/post", stream=True) + async def post(request): + assert request.stream.buffer_size == qsz + print("request.stream.buffer_size =", request.stream.buffer_size) + + bio = io.BytesIO() + while True: + bdata = await request.stream.read() + if not bdata: + break + bio.write(bdata) + + head = bdata[:3].decode("utf-8") + tail = bdata[3:][-3:].decode("utf-8") + print(head, "...", tail) + + bio.seek(0) + return text(bio.read().decode("utf-8")) + + request, response = app.test_client.post("/post", data=data) + assert response.status == 200 + assert response.text == data From 4c45d304007b67d0a6ae10b0faa0a9b5443b97e2 Mon Sep 17 00:00:00 2001 From: Seonghyeon Kim Date: Fri, 13 Dec 2019 01:24:11 +0900 Subject: [PATCH 18/19] FIX: invalid rst syntax (#1727) --- docs/sanic/request_data.rst | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/sanic/request_data.rst b/docs/sanic/request_data.rst index 7c32fa9b..43434472 100644 --- a/docs/sanic/request_data.rst +++ b/docs/sanic/request_data.rst @@ -224,20 +224,20 @@ The key difference when using this object is the distinction between the `get` a args.getlist('titles') # => ['Post 1', 'Post 2'] -```python -from sanic import Sanic -from sanic.response import json +.. code-block:: python + from sanic import Sanic + from sanic.response import json -app = Sanic(name="example") + app = Sanic(name="example") -@app.route("/") -def get_handler(request): - return json({ - "p1": request.args.getlist("p1") - }) -``` - -## Accessing the handler name with the request.endpoint attribute + @app.route("/") + def get_handler(request): + return json({ + "p1": request.args.getlist("p1") + }) + +Accessing the handler name with the request.endpoint attribute +-------------------------------------------------------------- The `request.endpoint` attribute holds the handler's name. For instance, the below route will return "hello". @@ -253,8 +253,7 @@ route will return "hello". def hello(request): return text(request.endpoint) -Or, with a blueprint it will be include both, separated by a period. For example, - the below route would return foo.bar: +Or, with a blueprint it will be include both, separated by a period. For example, the below route would return foo.bar: .. code-block:: python From 2d72874b0b95f58e0ef95358537f041bdb90e2a0 Mon Sep 17 00:00:00 2001 From: Adam Bannister Date: Thu, 12 Dec 2019 17:25:13 +0100 Subject: [PATCH 19/19] Add return type to Sanic.create_server for type hinting and docs (#1724) * add type hint and doc when create_server returns AsyncioServer * fix linting --- sanic/app.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 016ab73d..58c785b0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -24,7 +24,13 @@ from sanic.handlers import ErrorHandler from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.router import Router -from sanic.server import HttpProtocol, Signal, serve, serve_multiple +from sanic.server import ( + AsyncioServer, + HttpProtocol, + Signal, + serve, + serve_multiple, +) from sanic.static import register as static_register from sanic.testing import SanicASGITestClient, SanicTestClient from sanic.views import CompositionView @@ -1166,7 +1172,7 @@ class Sanic: access_log: Optional[bool] = None, return_asyncio_server=False, asyncio_server_kwargs=None, - ) -> None: + ) -> Optional[AsyncioServer]: """ Asynchronous version of :func:`run`. @@ -1206,7 +1212,7 @@ class Sanic: :param asyncio_server_kwargs: key-value arguments for asyncio/uvloop create_server method :type asyncio_server_kwargs: dict - :return: Nothing + :return: AsyncioServer if return_asyncio_server is true, else Nothing """ if sock is None: