diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index a912f3b9..157ee7cb 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -1,75 +1 @@ -# Extensions - -A list of Sanic extensions created by the community. - - -## Extension and Plugin Development - -- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins. -- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application. - -## Security - -- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic. -- [Secure](https://github.com/cakinney/secure): Secure 🔒 is a lightweight package that adds optional security headers and cookie attributes for Python web frameworks. -- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. Allows using redis, memcache or an in memory store. -- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. -- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for -- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request -- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic. -- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support. -- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic. -- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask. - -## Documentation - -- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. -- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation. -- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically. - -## ORM and Database Integration - -- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. -- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models. -- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic -- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/)) -- [Databases](https://github.com/encode/databases): Async database access for SQLAlchemy core, with support for PostgreSQL, MySQL, and SQLite. - -## Unit Testing - -- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously. - -## Project Creation Template - -- [cookiecutter-sanic](https://github.com/harshanarayana/cookiecutter-sanic): Get your sanic application up and running in a matter of second in a well defined project structure. - Batteries included for deployment, unit testing, automated release management and changelog generation. - -## Templating - -- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier. -- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. -- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/)) - -## API Helper Utilities - -- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic. -- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress. -- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support. -- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config. - -## i18n/l10n Support -- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the `Babel` library - -## Custom Middlewares - -- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. - -## Monitoring and Reporting - -- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic -- [sanic-zipkin](https://github.com/kevinqqnj/sanic-zipkin): Easily report request/function/RPC traces to zipkin/jaeger, through aiozipkin. - - -## Sample Applications - -- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose. +Moved to the [`awesome-sanic`](https://github.com/mekicha/awesome-sanic) list. \ No newline at end of file diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index 065d8e13..885cde71 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -193,7 +193,7 @@ 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"]}, + "args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]}, "query_string": "test1=value1&test2=&test3=value3" } ``` diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 54ea56a6..c016bd70 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -241,6 +241,45 @@ def handler(request): 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 diff --git a/sanic/blueprints.py b/sanic/blueprints.py index c50de648..b4894f62 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -37,7 +37,7 @@ class Blueprint: url_prefix=None, host=None, version=None, - strict_slashes=False, + strict_slashes=None, ): """ In *Sanic* terminology, a **Blueprint** is a logical collection of diff --git a/sanic/exceptions.py b/sanic/exceptions.py index b06c76d1..2c4ab2c0 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -218,6 +218,11 @@ class ContentRangeError(SanicException): } +@add_status_code(417) +class HeaderExpectationFailed(SanicException): + pass + + @add_status_code(403) class Forbidden(SanicException): pass diff --git a/sanic/request.py b/sanic/request.py index dfb3d1ff..15c2d5c4 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -29,7 +29,7 @@ except ImportError: DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" - +EXPECT_HEADER = "EXPECT" # HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 # > If the media type remains unknown, the recipient SHOULD treat it diff --git a/sanic/server.py b/sanic/server.py index c7e96676..5057f0b5 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -15,6 +15,7 @@ from httptools.parser.errors import HttpParserError from multidict import CIMultiDict from sanic.exceptions import ( + HeaderExpectationFailed, InvalidUsage, PayloadTooLarge, RequestTimeout, @@ -22,7 +23,7 @@ from sanic.exceptions import ( ServiceUnavailable, ) from sanic.log import access_logger, logger -from sanic.request import Request, StreamBuffer +from sanic.request import EXPECT_HEADER, Request, StreamBuffer from sanic.response import HTTPResponse @@ -314,6 +315,10 @@ class HttpProtocol(asyncio.Protocol): if self._keep_alive_timeout_handler: self._keep_alive_timeout_handler.cancel() self._keep_alive_timeout_handler = None + + if self.request.headers.get(EXPECT_HEADER): + self.expect_handler() + if self.is_request_stream: self._is_stream_handler = self.router.is_stream_handler( self.request @@ -324,6 +329,21 @@ class HttpProtocol(asyncio.Protocol): ) self.execute_request_handler() + def expect_handler(self): + """ + Handler for Expect Header. + """ + expect = self.request.headers.get(EXPECT_HEADER) + if self.request.version == "1.1": + if expect.lower() == "100-continue": + self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n") + else: + self.write_error( + HeaderExpectationFailed( + "Unknown Expect: {expect}".format(expect=expect) + ) + ) + def on_body(self, body): if self.is_request_stream and self._is_stream_handler: self._request_stream_task = self.loop.create_task( diff --git a/setup.py b/setup.py index 861ce013..63413d0e 100644 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ requirements = [ "aiofiles>=0.3.0", "websockets>=6.0,<7.0", "multidict>=4.0,<5.0", + "requests-async==0.5.0", ] tests_require = [ @@ -90,7 +91,6 @@ tests_require = [ "gunicorn", "pytest-cov", "httpcore==0.3.0", - "requests-async==0.5.0", "beautifulsoup4", uvloop, ujson, diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index f0a67bd7..16a97309 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -687,3 +687,49 @@ def test_register_blueprint(app, debug): "version 1.0. Please use the blueprint method" " instead" ) + + +def test_strict_slashes_behavior_adoption(app): + app.strict_slashes = True + + @app.get("/test") + def handler_test(request): + return text("Test") + + assert app.test_client.get("/test")[1].status == 200 + assert app.test_client.get("/test/")[1].status == 404 + + bp = Blueprint("bp") + + @bp.get("/one", strict_slashes=False) + def one(request): + return text("one") + + @bp.get("/second") + def second(request): + return text("second") + + app.blueprint(bp) + + assert app.test_client.get("/one")[1].status == 200 + assert app.test_client.get("/one/")[1].status == 200 + + assert app.test_client.get("/second")[1].status == 200 + assert app.test_client.get("/second/")[1].status == 404 + + bp2 = Blueprint("bp2", strict_slashes=False) + + @bp2.get("/third") + def third(request): + return text("third") + + app.blueprint(bp2) + assert app.test_client.get("/third")[1].status == 200 + assert app.test_client.get("/third/")[1].status == 200 + + @app.get("/f1", strict_slashes=False) + def f1(request): + return text("f1") + + assert app.test_client.get("/f1")[1].status == 200 + assert app.test_client.get("/f1/")[1].status == 200 diff --git a/tests/test_logo.py b/tests/test_logo.py index d99e117f..2f9cb90f 100644 --- a/tests/test_logo.py +++ b/tests/test_logo.py @@ -2,6 +2,7 @@ import asyncio import logging from sanic.config import BASE_LOGO +from sanic.testing import PORT try: @@ -13,7 +14,9 @@ except BaseException: def test_logo_base(app, caplog): - server = app.create_server(debug=True, return_asyncio_server=True) + server = app.create_server( + debug=True, return_asyncio_server=True, port=PORT + ) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False @@ -32,7 +35,9 @@ def test_logo_base(app, caplog): def test_logo_false(app, caplog): app.config.LOGO = False - server = app.create_server(debug=True, return_asyncio_server=True) + server = app.create_server( + debug=True, return_asyncio_server=True, port=PORT + ) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False @@ -45,13 +50,17 @@ def test_logo_false(app, caplog): app.stop() assert caplog.record_tuples[ROW][1] == logging.INFO - assert caplog.record_tuples[ROW][2] == "Goin' Fast @ http://127.0.0.1:8000" + assert caplog.record_tuples[ROW][ + 2 + ] == "Goin' Fast @ http://127.0.0.1:{}".format(PORT) def test_logo_true(app, caplog): app.config.LOGO = True - server = app.create_server(debug=True, return_asyncio_server=True) + server = app.create_server( + debug=True, return_asyncio_server=True, port=PORT + ) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False @@ -70,7 +79,9 @@ def test_logo_true(app, caplog): def test_logo_custom(app, caplog): app.config.LOGO = "My Custom Logo" - server = app.create_server(debug=True, return_asyncio_server=True) + server = app.create_server( + debug=True, return_asyncio_server=True, port=PORT + ) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index 65472a1e..17430fbc 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -1,4 +1,6 @@ +import pytest from sanic.blueprints import Blueprint +from sanic.exceptions import HeaderExpectationFailed from sanic.request import StreamBuffer from sanic.response import stream, text from sanic.views import CompositionView, HTTPMethodView @@ -40,6 +42,38 @@ def test_request_stream_method_view(app): assert response.text == data +@pytest.mark.parametrize("headers, expect_raise_exception", [ +({"EXPECT": "100-continue"}, False), +({"EXPECT": "100-continue-extra"}, True), +]) +def test_request_stream_100_continue(app, headers, expect_raise_exception): + class SimpleView(HTTPMethodView): + + @stream_decorator + async def post(self, request): + assert isinstance(request.stream, StreamBuffer) + result = "" + while True: + body = await request.stream.read() + if body is None: + break + result += body.decode("utf-8") + return text(result) + + app.add_route(SimpleView.as_view(), "/method_view") + + assert app.is_request_stream is True + + if not expect_raise_exception: + request, response = app.test_client.post("/method_view", data=data, headers={"EXPECT": "100-continue"}) + assert response.status == 200 + assert response.text == data + else: + with pytest.raises(ValueError) as e: + app.test_client.post("/method_view", data=data, headers={"EXPECT": "100-continue-extra"}) + assert "Unknown Expect: 100-continue-extra" in str(e) + + def test_request_stream_app(app): """for self.is_request_stream = True and decorators""" diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 412f9fa6..14798926 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -85,7 +85,7 @@ async def test_trigger_before_events_create_server(app): async def init_db(app, loop): app.db = MySanicDb() - await app.create_server(debug=True, return_asyncio_server=True) + await app.create_server(debug=True, return_asyncio_server=True, port=PORT) assert hasattr(app, "db") assert isinstance(app.db, MySanicDb)