Merge branch 'master' into asgi-refactor-attempt
This commit is contained in:
commit
b2d4132a14
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -218,6 +218,11 @@ class ContentRangeError(SanicException):
|
|||
}
|
||||
|
||||
|
||||
@add_status_code(417)
|
||||
class HeaderExpectationFailed(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
pass
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
2
setup.py
2
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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user