Merge branch 'master' into asgi-refactor-attempt

This commit is contained in:
Adam Hopkins 2019-05-27 21:03:23 +03:00 committed by GitHub
commit aebe2b5809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 256 additions and 282 deletions

19
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 90
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- bug
- urgent
- necessary
- help wanted
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. If this
is incorrect, please respond with an update. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -18,7 +18,7 @@ Sanic | Build fast. Run fast.
* - Support * - Support
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby| |Awesome| - | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby| |Awesome|
* - Stats * - Stats
- | |Downloads| - | |Downloads| |Conda downloads|
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg .. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
:target: https://community.sanicframework.org/ :target: https://community.sanicframework.org/
@ -50,6 +50,9 @@ Sanic | Build fast. Run fast.
.. |Downloads| image:: https://pepy.tech/badge/sanic/month .. |Downloads| image:: https://pepy.tech/badge/sanic/month
:alt: Downloads :alt: Downloads
:target: https://pepy.tech/project/sanic :target: https://pepy.tech/project/sanic
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
:alt: Downloads
:target: https://anaconda.org/conda-forge/sanic
.. end-badges .. end-badges
@ -57,7 +60,7 @@ Sanic is a **Python 3.6+** web server and web framework that's written to go fas
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_. `Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
The project is maintained by the community, for the community **Contributions are welcome!** The project is maintained by the community, for the community. **Contributions are welcome!**
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. 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.
@ -75,6 +78,12 @@ Installation
$ pip3 install --no-binary :all: sanic $ pip3 install --no-binary :all: sanic
.. note::
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.
Hello World Example Hello World Example
------------------- -------------------

25
SECURITY.md Normal file
View File

@ -0,0 +1,25 @@
# Security Policy
## Supported Versions
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
| Version | LTS | Supported |
| ------- | ------------------ | ------------------ |
| 19.6.0 | | :white_check_mark: |
| 19.3.1 | | :heavy_check_mark: |
| 18.12.0 | :heavy_check_mark: | :heavy_check_mark: |
| 0.8.3 | | :x: |
| 0.7.0 | | :x: |
| 0.6.0 | | :x: |
| 0.5.4 | | :x: |
| 0.4.1 | | :x: |
| 0.3.1 | | :x: |
| 0.2.0 | | :x: |
| 0.1.9 | | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
This will help to not publicize the issue until the team can address it and resolve it.

View File

@ -1,14 +1,16 @@
# Getting Started # Getting Started
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
least version 3.5 of Python before starting. Sanic uses the new `async`/`await` least version 3.6 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work. syntax, so earlier versions of python won't work.
## 1. Install Sanic ## 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.
pip3 install sanic
``` ```bash
pip3 install sanic
```
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables 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`) using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`)
@ -18,6 +20,13 @@ to true will stop that features installation.
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic 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` ## 2. Create a file called `main.py`
```python ```python

View File

@ -1,9 +1,9 @@
Sanic Sanic
================================= =================================
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 <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_. 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.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making 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 <https://github.com/channelcat/sanic/>`_. Contributions are welcome! Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
@ -22,4 +22,8 @@ Sanic aspires to be simple
return json({"hello": "world"}) return json({"hello": "world"})
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000) 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.

View File

@ -13,7 +13,7 @@ dependencies:
- sphinx==1.8.3 - sphinx==1.8.3
- sphinx_rtd_theme==0.4.2 - sphinx_rtd_theme==0.4.2
- recommonmark==0.5.0 - recommonmark==0.5.0
- requests-async==0.4.0 - requests-async==0.5.0
- sphinxcontrib-asyncio>=0.2.0 - sphinxcontrib-asyncio>=0.2.0
- docutils==0.14 - docutils==0.14
- pygments==2.3.1 - pygments==2.3.1

View File

@ -2,6 +2,6 @@ from sanic.app import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
__version__ = "19.03.1" __version__ = "19.6.0"
__all__ = ["Sanic", "Blueprint"] __all__ = ["Sanic", "Blueprint"]

View File

@ -443,8 +443,12 @@ class Sanic:
): ):
"""Decorate a function to be registered as a websocket route """Decorate a function to be registered as a websocket route
:param uri: path of the URL :param uri: path of the URL
:param host: Host IP or FQDN details
:param strict_slashes: If the API endpoint needs to terminate
with a "/" or not
:param subprotocols: optional list of str with supported subprotocols :param subprotocols: optional list of str with supported subprotocols
:param host: :param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: decorated function :return: decorated function
""" """
self.enable_websocket() self.enable_websocket()

View File

@ -89,8 +89,8 @@ tests_require = [
"multidict>=4.0,<5.0", "multidict>=4.0,<5.0",
"gunicorn", "gunicorn",
"pytest-cov", "pytest-cov",
"httpcore==0.1.1", "httpcore==0.3.0",
"requests-async==0.4.0", "requests-async==0.5.0",
"beautifulsoup4", "beautifulsoup4",
uvloop, uvloop,
ujson, ujson,

View File

@ -16,41 +16,28 @@ from sanic.response import text
from sanic.testing import HOST, PORT, SanicTestClient from sanic.testing import HOST, PORT, SanicTestClient
# import traceback
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
old_conn = None old_conn = None
class ReusableSanicConnectionPool(httpcore.ConnectionPool): class ReusableSanicConnectionPool(httpcore.ConnectionPool):
async def acquire_connection(self, url, ssl, timeout): async def acquire_connection(self, origin):
global old_conn global old_conn
if timeout.connect_timeout and not timeout.pool_timeout: connection = self.active_connections.pop_by_origin(origin, http2_only=True)
timeout.pool_timeout = timeout.connect_timeout if connection is None:
key = (url.scheme, url.hostname, url.port, ssl, timeout) connection = self.keepalive_connections.pop_by_origin(origin)
try:
connection = self._keepalive_connections[key].pop()
if not self._keepalive_connections[key]:
del self._keepalive_connections[key]
self.num_keepalive_connections -= 1
self.num_active_connections += 1
except (KeyError, IndexError): if connection is None:
ssl_context = await self.get_ssl_context(url, ssl) await self.max_connections.acquire()
try: connection = httpcore.HTTPConnection(
await asyncio.wait_for( origin,
self._max_connections.acquire(), timeout.pool_timeout ssl=self.ssl,
) timeout=self.timeout,
except asyncio.TimeoutError: backend=self.backend,
raise PoolTimeout() release_func=self.release_connection,
release = functools.partial(self.release_connection, key=key)
connection = httpcore.connections.Connection(
timeout=timeout, on_release=release
) )
self.num_active_connections += 1 self.active_connections.add(connection)
await connection.open(url.hostname, url.port, ssl=ssl_context)
if old_conn is not None: if old_conn is not None:
if old_conn != connection: if old_conn != connection:
@ -66,62 +53,6 @@ class ReusableSanicAdapter(requests.adapters.HTTPAdapter):
def __init__(self): def __init__(self):
self.pool = ReusableSanicConnectionPool() self.pool = ReusableSanicConnectionPool()
async def send(
self,
request,
stream=False,
timeout=None,
verify=True,
cert=None,
proxies=None,
):
method = request.method
url = request.url
headers = [
(_encode(k), _encode(v)) for k, v in request.headers.items()
]
if not request.body:
body = b""
elif isinstance(request.body, str):
body = _encode(request.body)
else:
body = request.body
if isinstance(timeout, tuple):
timeout_kwargs = {
"connect_timeout": timeout[0],
"read_timeout": timeout[1],
}
else:
timeout_kwargs = {
"connect_timeout": timeout,
"read_timeout": timeout,
}
ssl = httpcore.SSLConfig(cert=cert, verify=verify)
timeout = httpcore.TimeoutConfig(**timeout_kwargs)
try:
response = await self.pool.request(
method,
url,
headers=headers,
body=body,
stream=stream,
ssl=ssl,
timeout=timeout,
)
except (httpcore.BadResponse, socket.error) as err:
raise ConnectionError(err, request=request)
except httpcore.ConnectTimeout as err:
raise requests.exceptions.ConnectTimeout(err, request=request)
except httpcore.ReadTimeout as err:
raise requests.exceptions.ReadTimeout(err, request=request)
return self.build_response(request, response)
class ResusableSanicSession(requests.Session): class ResusableSanicSession(requests.Session):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
@ -149,13 +80,14 @@ class ReuseableSanicTestClient(SanicTestClient):
uri="/", uri="/",
gather_request=True, gather_request=True,
debug=False, debug=False,
server_kwargs={"return_asyncio_server": True}, server_kwargs=None,
*request_args, *request_args,
**request_kwargs, **request_kwargs,
): ):
loop = self._loop loop = self._loop
results = [None, None] results = [None, None]
exceptions = [] exceptions = []
server_kwargs = server_kwargs or {"return_asyncio_server": True}
if gather_request: if gather_request:
def _collect_request(request): def _collect_request(request):
@ -183,7 +115,6 @@ class ReuseableSanicTestClient(SanicTestClient):
) )
results[-1] = response results[-1] = response
except Exception as e2: except Exception as e2:
# traceback.print_tb(e2.__traceback__)
exceptions.append(e2) exceptions.append(e2)
if self._server is not None: if self._server is not None:
@ -201,7 +132,6 @@ class ReuseableSanicTestClient(SanicTestClient):
loop._stopping = False loop._stopping = False
_server = loop.run_until_complete(_server_co) _server = loop.run_until_complete(_server_co)
except Exception as e1: except Exception as e1:
# traceback.print_tb(e1.__traceback__)
raise e1 raise e1
self._server = _server self._server = _server
server.trigger_events(self.app.listeners["after_server_start"], loop) server.trigger_events(self.app.listeners["after_server_start"], loop)
@ -253,36 +183,32 @@ class ReuseableSanicTestClient(SanicTestClient):
request_keepalive = kwargs.pop( request_keepalive = kwargs.pop(
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"] "request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
) )
if self._session: if not self._session:
_session = self._session self._session = ResusableSanicSession()
else: try:
_session = ResusableSanicSession() response = await getattr(self._session, method.lower())(
self._session = _session url,
async with _session as session: verify=False,
try: timeout=request_keepalive,
response = await getattr(session, method.lower())( *args,
url, **kwargs,
verify=False, )
timeout=request_keepalive, except NameError:
*args, raise Exception(response.status_code)
**kwargs,
)
except NameError:
raise Exception(response.status_code)
try: try:
response.json = response.json() response.json = response.json()
except (JSONDecodeError, UnicodeDecodeError): except (JSONDecodeError, UnicodeDecodeError):
response.json = None response.json = None
response.body = await response.read() response.body = await response.read()
response.status = response.status_code response.status = response.status_code
response.content_type = response.headers.get("content-type") response.content_type = response.headers.get("content-type")
if raw_cookies: if raw_cookies:
response.raw_cookies = {} response.raw_cookies = {}
for cookie in response.cookies: for cookie in response.cookies:
response.raw_cookies[cookie.name] = cookie response.raw_cookies[cookie.name] = cookie
return response return response
@ -315,42 +241,46 @@ def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are """If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully both longer than the delay, the client _and_ server will successfully
reuse the existing connection.""" reuse the existing connection."""
loop = asyncio.new_event_loop() try:
asyncio.set_event_loop(loop) loop = asyncio.new_event_loop()
client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) asyncio.set_event_loop(loop)
headers = {"Connection": "keep-alive"} client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
request, response = client.get("/1", headers=headers) headers = {"Connection": "keep-alive"}
assert response.status == 200 request, response = client.get("/1", headers=headers)
assert response.text == "OK" assert response.status == 200
loop.run_until_complete(aio_sleep(1)) assert response.text == "OK"
request, response = client.get("/1") loop.run_until_complete(aio_sleep(1))
assert response.status == 200 request, response = client.get("/1")
assert response.text == "OK" assert response.status == 200
client.kill_server() assert response.text == "OK"
finally:
client.kill_server()
def test_keep_alive_client_timeout(): def test_keep_alive_client_timeout():
"""If the server keep-alive timeout is longer than the client """If the server keep-alive timeout is longer than the client
keep-alive timeout, client will try to create a new connection here.""" keep-alive timeout, client will try to create a new connection here."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
headers = {"Connection": "keep-alive"}
try: try:
request, response = client.get( loop = asyncio.new_event_loop()
"/1", headers=headers, request_keepalive=1 asyncio.set_event_loop(loop)
) client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
assert response.status == 200 headers = {"Connection": "keep-alive"}
assert response.text == "OK" try:
loop.run_until_complete(aio_sleep(2)) request, response = client.get(
exception = None "/1", headers=headers, request_keepalive=1
request, response = client.get("/1", request_keepalive=1) )
except ValueError as e: assert response.status == 200
exception = e assert response.text == "OK"
assert exception is not None loop.run_until_complete(aio_sleep(2))
assert isinstance(exception, ValueError) exception = None
assert "got a new connection" in exception.args[0] request, response = client.get("/1", request_keepalive=1)
client.kill_server() except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "got a new connection" in exception.args[0]
finally:
client.kill_server()
def test_keep_alive_server_timeout(): def test_keep_alive_server_timeout():
@ -358,25 +288,27 @@ def test_keep_alive_server_timeout():
keep-alive timeout, the client will either a 'Connection reset' error keep-alive timeout, the client will either a 'Connection reset' error
_or_ a new connection. Depending on how the event-loop handles the _or_ a new connection. Depending on how the event-loop handles the
broken server connection.""" broken server connection."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"}
try: try:
request, response = client.get( loop = asyncio.new_event_loop()
"/1", headers=headers, request_keepalive=60 asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"}
try:
request, response = client.get(
"/1", headers=headers, request_keepalive=60
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(3))
exception = None
request, response = client.get("/1", request_keepalive=60)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert (
"Connection reset" in exception.args[0]
or "got a new connection" in exception.args[0]
) )
assert response.status == 200 finally:
assert response.text == "OK" client.kill_server()
loop.run_until_complete(aio_sleep(3))
exception = None
request, response = client.get("/1", request_keepalive=60)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert (
"Connection reset" in exception.args[0]
or "got a new connection" in exception.args[0]
)
client.kill_server()

View File

@ -71,15 +71,13 @@ def test_request_stream_app(app):
@app.post("/post/<id>", stream=True) @app.post("/post/<id>", stream=True)
async def post(request, id): async def post(request, id):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
@app.put("/_put") @app.put("/_put")
async def _put(request): async def _put(request):
@ -89,15 +87,13 @@ def test_request_stream_app(app):
@app.put("/put", stream=True) @app.put("/put", stream=True)
async def put(request): async def put(request):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
@app.patch("/_patch") @app.patch("/_patch")
async def _patch(request): async def _patch(request):
@ -107,15 +103,14 @@ def test_request_stream_app(app):
@app.patch("/patch", stream=True) @app.patch("/patch", stream=True)
async def patch(request): async def patch(request):
assert isinstance(request.stream, StreamBuffer) 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)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
assert app.is_request_stream is True assert app.is_request_stream is True
@ -166,15 +161,13 @@ def test_request_stream_handle_exception(app):
@app.post("/post/<id>", stream=True) @app.post("/post/<id>", stream=True)
async def post(request, id): async def post(request, id):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
# 404 # 404
request, response = app.test_client.post("/in_valid_post", data=data) request, response = app.test_client.post("/in_valid_post", data=data)
@ -222,15 +215,13 @@ def test_request_stream_blueprint(app):
@bp.post("/post/<id>", stream=True) @bp.post("/post/<id>", stream=True)
async def post(request, id): async def post(request, id):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
@bp.put("/_put") @bp.put("/_put")
async def _put(request): async def _put(request):
@ -240,15 +231,13 @@ def test_request_stream_blueprint(app):
@bp.put("/put", stream=True) @bp.put("/put", stream=True)
async def put(request): async def put(request):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
@bp.patch("/_patch") @bp.patch("/_patch")
async def _patch(request): async def _patch(request):
@ -258,27 +247,23 @@ def test_request_stream_blueprint(app):
@bp.patch("/patch", stream=True) @bp.patch("/patch", stream=True)
async def patch(request): async def patch(request):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
async def post_add_route(request): async def post_add_route(request):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
bp.add_route( bp.add_route(
post_add_route, "/post/add_route", methods=["POST"], stream=True post_add_route, "/post/add_route", methods=["POST"], stream=True
@ -388,15 +373,13 @@ def test_request_stream(app):
@app.post("/stream", stream=True) @app.post("/stream", stream=True)
async def handler(request): async def handler(request):
assert isinstance(request.stream, StreamBuffer) assert isinstance(request.stream, StreamBuffer)
result = ""
async def streaming(response): while True:
while True: body = await request.stream.read()
body = await request.stream.read() if body is None:
if body is None: break
break result += body.decode("utf-8")
await response.write(body.decode("utf-8")) return text(result)
return stream(streaming)
@app.get("/get") @app.get("/get")
async def get(request): async def get(request):

View File

@ -13,37 +13,26 @@ class DelayableSanicConnectionPool(httpcore.ConnectionPool):
self._request_delay = request_delay self._request_delay = request_delay
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
async def request( async def send(
self, self,
method, request,
url,
headers=(),
body=b"",
stream=False, stream=False,
ssl=None, ssl=None,
timeout=None, timeout=None,
): ):
if ssl is None: connection = await self.acquire_connection(request.url.origin)
ssl = self.ssl_config if connection.h11_connection is None and connection.h2_connection is None:
if timeout is None: await connection.connect(ssl=ssl, timeout=timeout)
timeout = self.timeout
parsed_url = httpcore.URL(url)
request = httpcore.Request(
method, parsed_url, headers=headers, body=body
)
connection = await self.acquire_connection(
parsed_url, ssl=ssl, timeout=timeout
)
if self._request_delay: if self._request_delay:
print(f"\t>> Sleeping ({self._request_delay})")
await asyncio.sleep(self._request_delay) await asyncio.sleep(self._request_delay)
response = await connection.send(request) try:
if not stream: response = await connection.send(
try: request, stream=stream, ssl=ssl, timeout=timeout
await response.read() )
finally: except BaseException as exc:
await response.close() self.active_connections.remove(connection)
self.max_connections.release()
raise exc
return response return response

View File

@ -12,8 +12,8 @@ deps =
pytest-cov pytest-cov
pytest-sanic pytest-sanic
pytest-sugar pytest-sugar
httpcore==0.1.1 httpcore==0.3.0
requests-async==0.4.0 requests-async==0.5.0
chardet<=2.3.0 chardet<=2.3.0
beautifulsoup4 beautifulsoup4
gunicorn gunicorn