Merge branch 'master' into asgi-refactor-attempt

This commit is contained in:
Adam Hopkins 2019-06-11 11:11:32 +03:00 committed by GitHub
commit b2d4132a14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 167 additions and 86 deletions

View File

@ -1,75 +1 @@
# Extensions Moved to the [`awesome-sanic`](https://github.com/mekicha/awesome-sanic) list.
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.

View File

@ -193,7 +193,7 @@ The output will be:
{ {
"parsed": true, "parsed": true,
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3", "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" "query_string": "test1=value1&test2=&test3=value3"
} }
``` ```

View File

@ -241,6 +241,45 @@ def handler(request):
app.blueprint(bp) 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 ## User defined route name
A custom route name can be used by passing a `name` argument while registering the route which will A custom route name can be used by passing a `name` argument while registering the route which will

View File

@ -37,7 +37,7 @@ class Blueprint:
url_prefix=None, url_prefix=None,
host=None, host=None,
version=None, version=None,
strict_slashes=False, strict_slashes=None,
): ):
""" """
In *Sanic* terminology, a **Blueprint** is a logical collection of In *Sanic* terminology, a **Blueprint** is a logical collection of

View File

@ -218,6 +218,11 @@ class ContentRangeError(SanicException):
} }
@add_status_code(417)
class HeaderExpectationFailed(SanicException):
pass
@add_status_code(403) @add_status_code(403)
class Forbidden(SanicException): class Forbidden(SanicException):
pass pass

View File

@ -29,7 +29,7 @@ except ImportError:
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" 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 # 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 # > If the media type remains unknown, the recipient SHOULD treat it

View File

@ -15,6 +15,7 @@ from httptools.parser.errors import HttpParserError
from multidict import CIMultiDict from multidict import CIMultiDict
from sanic.exceptions import ( from sanic.exceptions import (
HeaderExpectationFailed,
InvalidUsage, InvalidUsage,
PayloadTooLarge, PayloadTooLarge,
RequestTimeout, RequestTimeout,
@ -22,7 +23,7 @@ from sanic.exceptions import (
ServiceUnavailable, ServiceUnavailable,
) )
from sanic.log import access_logger, logger 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 from sanic.response import HTTPResponse
@ -314,6 +315,10 @@ class HttpProtocol(asyncio.Protocol):
if self._keep_alive_timeout_handler: if self._keep_alive_timeout_handler:
self._keep_alive_timeout_handler.cancel() self._keep_alive_timeout_handler.cancel()
self._keep_alive_timeout_handler = None self._keep_alive_timeout_handler = None
if self.request.headers.get(EXPECT_HEADER):
self.expect_handler()
if self.is_request_stream: if self.is_request_stream:
self._is_stream_handler = self.router.is_stream_handler( self._is_stream_handler = self.router.is_stream_handler(
self.request self.request
@ -324,6 +329,21 @@ class HttpProtocol(asyncio.Protocol):
) )
self.execute_request_handler() 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): def on_body(self, body):
if self.is_request_stream and self._is_stream_handler: if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task( self._request_stream_task = self.loop.create_task(

View File

@ -82,6 +82,7 @@ requirements = [
"aiofiles>=0.3.0", "aiofiles>=0.3.0",
"websockets>=6.0,<7.0", "websockets>=6.0,<7.0",
"multidict>=4.0,<5.0", "multidict>=4.0,<5.0",
"requests-async==0.5.0",
] ]
tests_require = [ tests_require = [
@ -90,7 +91,6 @@ tests_require = [
"gunicorn", "gunicorn",
"pytest-cov", "pytest-cov",
"httpcore==0.3.0", "httpcore==0.3.0",
"requests-async==0.5.0",
"beautifulsoup4", "beautifulsoup4",
uvloop, uvloop,
ujson, ujson,

View File

@ -687,3 +687,49 @@ def test_register_blueprint(app, debug):
"version 1.0. Please use the blueprint method" "version 1.0. Please use the blueprint method"
" instead" " 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

View File

@ -2,6 +2,7 @@ import asyncio
import logging import logging
from sanic.config import BASE_LOGO from sanic.config import BASE_LOGO
from sanic.testing import PORT
try: try:
@ -13,7 +14,9 @@ except BaseException:
def test_logo_base(app, caplog): 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() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop._stopping = False loop._stopping = False
@ -32,7 +35,9 @@ def test_logo_base(app, caplog):
def test_logo_false(app, caplog): def test_logo_false(app, caplog):
app.config.LOGO = False 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() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop._stopping = False loop._stopping = False
@ -45,13 +50,17 @@ def test_logo_false(app, caplog):
app.stop() app.stop()
assert caplog.record_tuples[ROW][1] == logging.INFO 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): def test_logo_true(app, caplog):
app.config.LOGO = True 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() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop._stopping = False loop._stopping = False
@ -70,7 +79,9 @@ def test_logo_true(app, caplog):
def test_logo_custom(app, caplog): def test_logo_custom(app, caplog):
app.config.LOGO = "My Custom Logo" 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() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop._stopping = False loop._stopping = False

View File

@ -1,4 +1,6 @@
import pytest
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.exceptions import HeaderExpectationFailed
from sanic.request import StreamBuffer from sanic.request import StreamBuffer
from sanic.response import stream, text from sanic.response import stream, text
from sanic.views import CompositionView, HTTPMethodView from sanic.views import CompositionView, HTTPMethodView
@ -40,6 +42,38 @@ def test_request_stream_method_view(app):
assert response.text == data 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): def test_request_stream_app(app):
"""for self.is_request_stream = True and decorators""" """for self.is_request_stream = True and decorators"""

View File

@ -85,7 +85,7 @@ async def test_trigger_before_events_create_server(app):
async def init_db(app, loop): async def init_db(app, loop):
app.db = MySanicDb() 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 hasattr(app, "db")
assert isinstance(app.db, MySanicDb) assert isinstance(app.db, MySanicDb)