diff --git a/README.rst b/README.rst
index 311be757..b064612d 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,44 @@
Sanic
=====
-|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
+.. start-badges
+
+.. list-table::
+ :stub-columns: 1
+
+ * - Build
+ - | |Build Status| |AppVeyor Build Status| |Codecov|
+ * - Docs
+ - |Documentation|
+ * - Package
+ - | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
+ * - Support
+ - |Join the chat at https://gitter.im/sanic-python/Lobby|
+
+.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
+ :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/huge-success/sanic
+.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
+ :target: https://travis-ci.org/huge-success/sanic
+.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
+ :target: https://ci.appveyor.com/project/huge-success/sanic
+.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
+ :target: http://sanic.readthedocs.io/en/latest/?badge=latest
+.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
+ :target: https://pypi.python.org/pypi/sanic/
+.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
+ :target: https://pypi.python.org/pypi/sanic/
+.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+.. |Wheel| image:: https://img.shields.io/pypi/wheel/sanic.svg
+ :alt: PyPI Wheel
+ :target: https://pypi.python.org/pypi/sanic
+.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
+ :alt: Supported implementations
+ :target: https://pypi.python.org/pypi/sanic
+
+.. end-badges
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article `_.
@@ -45,22 +82,6 @@ Documentation
`Documentation on Readthedocs `_.
-.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
- :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
-.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
- :target: https://codecov.io/gh/huge-success/sanic
-.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
- :target: https://travis-ci.org/huge-success/sanic
-.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
- :target: https://ci.appveyor.com/project/huge-success/sanic
-.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
- :target: http://sanic.readthedocs.io/en/latest/?badge=latest
-.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
- :target: https://pypi.python.org/pypi/sanic/
-.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
- :target: https://pypi.python.org/pypi/sanic/
-.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
- :target: https://github.com/ambv/black
Questions and Discussion
------------------------
diff --git a/docs/sanic/config.md b/docs/sanic/config.md
index c16e2397..5f87348a 100644
--- a/docs/sanic/config.md
+++ b/docs/sanic/config.md
@@ -88,6 +88,7 @@ Out of the box there are just a few predefined values which can be overwritten w
| 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 |
diff --git a/docs/sanic/streaming.md b/docs/sanic/streaming.md
index bf3ca664..53eebad7 100644
--- a/docs/sanic/streaming.md
+++ b/docs/sanic/streaming.md
@@ -2,7 +2,7 @@
## Request Streaming
-Sanic allows you to get request data by stream, as below. When the request ends, `request.stream.get()` returns `None`. Only post, put and patch decorator have stream argument.
+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
@@ -22,7 +22,7 @@ class SimpleView(HTTPMethodView):
async def post(self, request):
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
@@ -33,7 +33,7 @@ class SimpleView(HTTPMethodView):
async def handler(request):
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
@@ -45,7 +45,7 @@ async def handler(request):
async def bp_handler(request):
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
@@ -55,7 +55,7 @@ async def bp_handler(request):
async def post_handler(request):
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
diff --git a/sanic/app.py b/sanic/app.py
index c2a24464..7a4af641 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -1071,6 +1071,7 @@ class Sanic:
"response_timeout": self.config.RESPONSE_TIMEOUT,
"keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT,
"request_max_size": self.config.REQUEST_MAX_SIZE,
+ "request_buffer_queue_size": self.config.REQUEST_BUFFER_QUEUE_SIZE,
"keep_alive": self.config.KEEP_ALIVE,
"loop": loop,
"register_sys_signals": register_sys_signals,
diff --git a/sanic/config.py b/sanic/config.py
index 0a53a607..15341829 100644
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -32,6 +32,7 @@ class Config(dict):
▀▀▄▄▀
"""
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
+ self.REQUEST_BUFFER_QUEUE_SIZE = 100
self.REQUEST_TIMEOUT = 60 # 60 seconds
self.RESPONSE_TIMEOUT = 60 # 60 seconds
self.KEEP_ALIVE = keep_alive
diff --git a/sanic/request.py b/sanic/request.py
index 6ee95036..42237d32 100644
--- a/sanic/request.py
+++ b/sanic/request.py
@@ -1,3 +1,4 @@
+import asyncio
import json
import sys
@@ -47,6 +48,23 @@ class RequestParameters(dict):
return super().get(name, default)
+class StreamBuffer:
+ def __init__(self, buffer_size=100):
+ self._queue = asyncio.Queue(buffer_size)
+
+ async def read(self):
+ """ Stop reading when gets None """
+ payload = await self._queue.get()
+ self._queue.task_done()
+ return payload
+
+ async def put(self, payload):
+ await self._queue.put(payload)
+
+ def is_full(self):
+ return self._queue.full()
+
+
class Request(dict):
"""Properties of an HTTP request such as URL, headers, etc."""
diff --git a/sanic/server.py b/sanic/server.py
index ceefa4df..70aba8a9 100644
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -22,7 +22,7 @@ from sanic.exceptions import (
ServiceUnavailable,
)
from sanic.log import access_logger, logger
-from sanic.request import Request
+from sanic.request import Request, StreamBuffer
from sanic.response import HTTPResponse
@@ -59,6 +59,7 @@ class HttpProtocol(asyncio.Protocol):
"response_timeout",
"keep_alive_timeout",
"request_max_size",
+ "request_buffer_queue_size",
"request_class",
"is_request_stream",
"router",
@@ -89,11 +90,12 @@ class HttpProtocol(asyncio.Protocol):
request_handler,
error_handler,
signal=Signal(),
- connections=set(),
+ connections=None,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
request_max_size=None,
+ request_buffer_queue_size=100,
request_class=None,
access_log=True,
keep_alive=True,
@@ -112,10 +114,11 @@ class HttpProtocol(asyncio.Protocol):
self.router = router
self.signal = signal
self.access_log = access_log
- self.connections = connections
+ self.connections = connections or set()
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
+ self.request_buffer_queue_size = request_buffer_queue_size
self.response_timeout = response_timeout
self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size
@@ -298,16 +301,26 @@ class HttpProtocol(asyncio.Protocol):
self.request
)
if self._is_stream_handler:
- self.request.stream = asyncio.Queue()
+ self.request.stream = StreamBuffer(
+ self.request_buffer_queue_size
+ )
self.execute_request_handler()
def on_body(self, body):
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
- self.request.stream.put(body)
+ self.body_append(body)
)
- return
- self.request.body_push(body)
+ 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)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
@@ -575,6 +588,7 @@ def serve(
ssl=None,
sock=None,
request_max_size=None,
+ request_buffer_queue_size=100,
reuse_port=False,
loop=None,
protocol=HttpProtocol,
@@ -635,6 +649,7 @@ def serve(
outgoing bytes, the low-water limit is a
quarter of the high-water limit.
:param is_request_stream: disable/enable Request.stream
+ :param request_buffer_queue_size: streaming request buffer queue size
:param router: Router object
:param graceful_shutdown_timeout: How long take to Force close non-idle
connection
diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py
index f46e235b..ecf201f2 100644
--- a/tests/test_request_stream.py
+++ b/tests/test_request_stream.py
@@ -4,8 +4,10 @@ from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.response import stream, text
+from sanic.request import StreamBuffer
-data = "abc" * 100000
+
+data = "abc" * 10000000
def test_request_stream_method_view(app):
@@ -19,10 +21,10 @@ def test_request_stream_method_view(app):
@stream_decorator
async def post(self, request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
@@ -71,11 +73,11 @@ def test_request_stream_app(app):
@app.post('/post/', stream=True)
async def post(request, id):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -88,11 +90,11 @@ def test_request_stream_app(app):
@app.put('/put', stream=True)
async def put(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -105,11 +107,11 @@ def test_request_stream_app(app):
@app.patch('/patch', stream=True)
async def patch(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -163,11 +165,11 @@ def test_request_stream_handle_exception(app):
@app.post('/post/', stream=True)
async def post(request, id):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -216,11 +218,11 @@ def test_request_stream_blueprint(app):
@bp.post('/post/', stream=True)
async def post(request, id):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -233,11 +235,11 @@ def test_request_stream_blueprint(app):
@bp.put('/put', stream=True)
async def put(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -250,11 +252,11 @@ def test_request_stream_blueprint(app):
@bp.patch('/patch', stream=True)
async def patch(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -313,10 +315,10 @@ def test_request_stream_composition_view(app):
return text('OK')
async def post_handler(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
@@ -350,10 +352,10 @@ def test_request_stream(app):
@stream_decorator
async def post(self, request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
@@ -361,11 +363,11 @@ def test_request_stream(app):
@app.post('/stream', stream=True)
async def handler(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
await response.write(body.decode('utf-8'))
@@ -378,10 +380,10 @@ def test_request_stream(app):
@bp.post('/bp_stream', stream=True)
async def bp_stream(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
@@ -397,10 +399,10 @@ def test_request_stream(app):
return text('OK')
async def post_handler(request):
- assert isinstance(request.stream, asyncio.Queue)
+ assert isinstance(request.stream, StreamBuffer)
result = ''
while True:
- body = await request.stream.get()
+ body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')