Skip to content

Commit

Permalink
release 0.0.310 (#42)
Browse files Browse the repository at this point in the history
* clear previous headers

* optimize

---------

Co-authored-by: nggit <[email protected]>
  • Loading branch information
nggit and nggit authored Nov 25, 2023
1 parent 58b1c12 commit b0f8304
Show file tree
Hide file tree
Showing 12 changed files with 40 additions and 25 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ And other use cases…
## Features
Tremolo is only suitable for those who value [minimalism](https://en.wikipedia.org/wiki/Minimalism_%28computing%29) and stability over features.

With only **3k** lines of code, with no dependencies other than the [Python Standard Library](https://docs.python.org/3/library/index.html), it gives you:
With only **3k** lines of code, with **no dependencies** other than the [Python Standard Library](https://docs.python.org/3/library/index.html), it gives you:

* HTTP/1.x with [WebSocket support](https://nggit.github.io/tremolo-docs/websocket.html)
* Keep-Alive connections with [configurable limit](https://nggit.github.io/tremolo-docs/configuration.html#keepalive_connections)
* Stream chunked uploads
* [Stream multipart uploads](https://nggit.github.io/tremolo-docs/body.html#multipart)
* Download/upload speed throttling
* [Resumable downloads](https://nggit.github.io/tremolo-docs/resumable-downloads.html)
* Framework features; routing, middleware, etc
* Framework features; routing, middleware, etc.
* ASGI server
* PyPy compatible

Expand Down Expand Up @@ -139,7 +139,7 @@ You will find that Tremolo is reasonably fast.
However, it should be noted that bottlenecks often occur on the application side.
Which means that in real-world usage, throughput reflects more on the application than the server.

## Misc
## Misc.
Tremolo utilizes `SO_REUSEPORT` (Linux 3.9+) to load balance worker processes.

```python
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='tremolo',
version='0.0.309',
version='0.0.310',
license='MIT',
author='nggit',
author_email='[email protected]',
Expand Down
2 changes: 2 additions & 0 deletions tests/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ async def my_response_middleware(**server):
response.set_status(503, b'Under Maintenance')
response.set_content_type(b'text/plain')

return b'Under Maintenance'


@app.route('/getheaderline')
async def get_headerline(**server):
Expand Down
5 changes: 3 additions & 2 deletions tests/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ def test_get_ok_10(self):

self.assertEqual(
header[:header.find(b'\r\n')],
b'HTTP/1.0 503 Under Maintenance'
b'HTTP/1.0 503 Service Unavailable'
)
self.assertTrue(b'\r\nContent-Type: text/plain' in header)
self.assertFalse(chunked_detected(header))
self.assertEqual(body, b'Under Maintenance')

# these values are set by the request and response middleware
self.assertTrue(b'\r\nX-Foo: baz' in header and
Expand Down Expand Up @@ -174,7 +175,7 @@ def test_head_10(self):
version='1.0')

self.assertEqual(header[:header.find(b'\r\n')],
b'HTTP/1.0 503 Under Maintenance')
b'HTTP/1.0 503 Service Unavailable')
self.assertTrue(b'\r\nContent-Length: ' in header)
self.assertTrue(b'\r\nContent-Type: text/plain' in header)
self.assertFalse(b'\r\nTransfer-Encoding: chunked\r\n' in header)
Expand Down
2 changes: 1 addition & 1 deletion tremolo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '0.0.309'
__version__ = '0.0.310'

from .tremolo import Tremolo # noqa: E402
from . import exceptions # noqa: E402,F401
Expand Down
4 changes: 2 additions & 2 deletions tremolo/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, lock=None, **kwargs):

async def _connection_made(self):
for func, _ in self._middlewares['connect']:
if (await func(**self._server)):
if await func(**self._server):
break

async def _connection_lost(self, exc):
Expand All @@ -35,7 +35,7 @@ async def _connection_lost(self, exc):
while i > 0:
i -= 1

if (await self._middlewares['close'][i][0](**self._server)):
if await self._middlewares['close'][i][0](**self._server):
break
finally:
super().connection_lost(exc)
Expand Down
2 changes: 1 addition & 1 deletion tremolo/lib/h1parser/parse_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def getlist(self, name):

return result

return values.replace(b', ', b',').split(b',')
return values.replace(b', ', b',').split(b',', 100)


class ParseHeader:
Expand Down
3 changes: 2 additions & 1 deletion tremolo/lib/http_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ async def handle_exception(self, exc):
exc = InternalServerError(cause=exc)

if self.request is not None and self.response is not None:
self.response.headers.clear()
self.response.set_status(exc.code, exc.message)
self.response.set_content_type(exc.content_type)
data = b''
Expand All @@ -214,7 +215,7 @@ async def handle_exception(self, exc):
if isinstance(data, str):
encoding = 'utf-8'

for v in exc.content_type.split(';'):
for v in exc.content_type.split(';', 100):
v = v.lstrip()

if v.startswith('charset='):
Expand Down
16 changes: 8 additions & 8 deletions tremolo/lib/http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,12 @@ async def stream(self, raw=False):
if not paused:
try:
buf.extend(await agen.__anext__())
except StopAsyncIteration:
except StopAsyncIteration as exc:
if b'0\r\n' not in buf:
del buf[:]
raise BadRequest(
'bad chunked encoding: incomplete read'
)
) from exc

if unread_bytes > 0:
data = buf[:unread_bytes]
Expand Down Expand Up @@ -215,9 +215,9 @@ async def stream(self, raw=False):

try:
chunk_size = int(buf[:i].split(b';', 1)[0], 16)
except ValueError:
except ValueError as exc:
del buf[:]
raise BadRequest('bad chunked encoding')
raise BadRequest('bad chunked encoding') from exc

data = buf[i + 2:i + 2 + chunk_size]
unread_bytes = chunk_size - len(data)
Expand Down Expand Up @@ -321,8 +321,8 @@ async def files(self, limit=1024):

try:
boundary = ct['boundary'][-1].encode('latin-1')
except KeyError:
raise BadRequest('missing boundary')
except KeyError as exc:
raise BadRequest('missing boundary') from exc

header = None
body = bytearray()
Expand All @@ -341,12 +341,12 @@ async def files(self, limit=1024):
if not paused:
try:
data = await self._read_instance.__anext__()
except StopAsyncIteration:
except StopAsyncIteration as exc:
if header_size == -1 or body_size == -1:
del body[:]
raise BadRequest(
'malformed multipart/form-data: incomplete read'
)
) from exc

if header is None:
self._read_buf.extend(data)
Expand Down
12 changes: 11 additions & 1 deletion tremolo/lib/http_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ def set_status(self, status=200, message=b'OK'):
self._status.append((status, message))

def get_status(self):
if b'_line' in self.headers:
_, status, message = self.headers.pop(b'_line')
return int(status), message

try:
return self._status.pop()
except IndexError:
Expand All @@ -153,6 +157,9 @@ def set_content_type(self, content_type=b'text/html; charset=utf-8'):
self._content_type.append(content_type)

def get_content_type(self):
if b'content-type' in self.headers:
return self.headers.pop(b'content-type')[0][13:].lstrip()

try:
return self._content_type.pop()
except IndexError:
Expand Down Expand Up @@ -192,6 +199,8 @@ async def end(self, data=b'', keepalive=True, **kwargs):
):
data = b''

excludes = (b'connection', b'content-length', b'transfer-encoding')

await self.send(
b'HTTP/%s %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n'
b'Connection: %s\r\n%s\r\n\r\n%s' % (
Expand All @@ -202,7 +211,8 @@ async def end(self, data=b'', keepalive=True, **kwargs):
KEEPALIVE_OR_CLOSE[
keepalive and self._request.http_keepalive],
b'\r\n'.join(
b'\r\n'.join(v) for v in self.headers.values()),
b'\r\n'.join(v) for k, v in self.headers.items() if
k not in excludes),
data), throttle=False, **kwargs
)

Expand Down
4 changes: 2 additions & 2 deletions tremolo/lib/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ async def recv(self):
data = await task

self.protocol.queue[0].task_done()
except asyncio.CancelledError:
raise TimeoutError('recv timeout')
except asyncio.CancelledError as exc:
raise TimeoutError('recv timeout') from exc
finally:
timer.cancel()

Expand Down
7 changes: 4 additions & 3 deletions tremolo/lib/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ async def receive(self):

try:
payload = await self.recv()
except TimeoutError:
raise WebSocketServerClosed('receive timeout', code=1000)
except TimeoutError as exc:
raise WebSocketServerClosed('receive timeout',
code=1000) from exc
finally:
timer.cancel()

Expand Down Expand Up @@ -166,7 +167,7 @@ def _ping(self):
# ping only if this connection is still listed,
# otherwise let the recv timeout drop it
if self.protocol in self.protocol.options['_connections']:
return self.protocol.loop.create_task(self.ping())
self.protocol.loop.create_task(self.ping())

async def ping(self, data=b''):
await self.send(data, opcode=9)
Expand Down

0 comments on commit b0f8304

Please sign in to comment.