diff --git a/examples/hello_asgi.py b/examples/hello_asgi.py
new file mode 100644
index 0000000..39e1b01
--- /dev/null
+++ b/examples/hello_asgi.py
@@ -0,0 +1,37 @@
+try:
+ import uasyncio as asyncio
+except ImportError:
+ import asyncio
+from microdot_asgi import Microdot, Response
+
+app = Microdot()
+
+htmldoc = '''
+
+
+ Microdot Example Page
+
+
+
+
+
+'''
+
+
+@app.route('/')
+async def hello(request):
+ return htmldoc, 200, {'Content-Type': 'text/html'}
+
+
+@app.route('/shutdown')
+async def shutdown(request):
+ request.app.shutdown()
+ return 'The server is shutting down...'
+
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/examples/hello_async.py b/examples/hello_async.py
index 3aabc55..1ef055d 100644
--- a/examples/hello_async.py
+++ b/examples/hello_async.py
@@ -24,7 +24,7 @@ htmldoc = '''
@app.route('/')
async def hello(request):
- return Response(body=htmldoc, headers={'Content-Type': 'text/html'})
+ return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')
diff --git a/src/microdot.py b/src/microdot.py
index 2511947..978c7ea 100644
--- a/src/microdot.py
+++ b/src/microdot.py
@@ -440,6 +440,8 @@ class Response():
stream.write('{header}: {value}\r\n'.format(
header=header, value=value).encode())
stream.write(b'\r\n')
+
+ # body
for body in self.body_iter():
stream.write(body)
diff --git a/src/microdot_asgi.py b/src/microdot_asgi.py
new file mode 100644
index 0000000..a9cf0ad
--- /dev/null
+++ b/src/microdot_asgi.py
@@ -0,0 +1,126 @@
+import logging
+import os
+import signal
+from microdot_asyncio import * # noqa: F401, F403
+from microdot_asyncio import Microdot as BaseMicrodot
+from microdot_asyncio import Request
+
+
+class _BodyStream: # pragma: no cover
+ def __init__(self, receive):
+ self.receive = receive
+ self.data = b''
+ self.more = True
+
+ async def read_more(self):
+ if self.more:
+ packet = await self.receive()
+ self.data += packet.get('body', b'')
+ self.more = packet.get('more_body', False)
+
+ async def read(self, n=-1):
+ while self.more and len(self.data) < n:
+ self.read_more()
+ if len(self.data) < n:
+ data = self.data
+ self.data = b''
+ return data
+
+ data = self.data[:n]
+ self.data = self.data[n:]
+ return data
+
+ async def readline(self):
+ return self.readuntil()
+
+ async def readexactly(self, n):
+ return self.read(n)
+
+ async def readuntil(self, separator=b'\n'):
+ if self.more and separator not in self.data:
+ self.read_more()
+ data, self.data = self.data.split(separator, 1)
+ return data
+
+
+class Microdot(BaseMicrodot):
+ async def asgi_app(self, scope, receive, send):
+ if scope['type'] != 'http': # pragma: no cover
+ return
+ path = scope['path']
+ if 'query_string' in scope and scope['query_string']:
+ path += '?' + scope['query_string'].decode()
+ headers = {}
+ content_length = 0
+ for key, value in scope.get('headers', []):
+ headers[key] = value
+ if key.lower() == 'content-length':
+ content_length = int(value)
+
+ body = b''
+ if content_length and content_length <= Request.max_body_length:
+ body = b''
+ more = True
+ while more:
+ packet = await receive()
+ body += packet.get('body', b'')
+ more = packet.get('more_body', False)
+ stream = None
+ else:
+ body = b''
+ stream = _BodyStream(receive)
+
+ req = Request(
+ self,
+ (scope['client'][0], scope['client'][1]),
+ scope['method'],
+ path,
+ 'HTTP/' + scope['http_version'],
+ headers,
+ body=body,
+ stream=stream)
+ req.asgi_scope = scope
+
+ res = await self.dispatch_request(req)
+ res.complete()
+
+ await send({'type': 'http.response.start',
+ 'status': res.status_code,
+ 'headers': [(name, value)
+ for name, value in res.headers.items()]})
+ body_iter = res.body_iter()
+ body = b''
+ try:
+ body += await body_iter.__anext__()
+ while True:
+ next_body = await body_iter.__anext__()
+ await send({'type': 'http.response.body',
+ 'body': body,
+ 'more_body': True})
+ body = next_body
+ except StopAsyncIteration:
+ await send({'type': 'http.response.body',
+ 'body': body,
+ 'more_body': False})
+
+ async def __call__(self, scope, receive, send):
+ return await self.asgi_app(scope, receive, send)
+
+ def shutdown(self):
+ pid = os.getpgrp() if hasattr(os, 'getpgrp') else os.getpid()
+ os.kill(pid, signal.SIGTERM)
+
+ def run(self, host='0.0.0.0', port=5000, debug=False,
+ **options): # pragma: no cover
+ try:
+ from waitress import serve
+ except ImportError: # pragma: no cover
+ raise RuntimeError('The run() method requires Waitress to be '
+ 'installed (i.e. run "pip install waitress").')
+
+ self.debug = debug
+ if debug:
+ logger = logging.getLogger('waitress')
+ logger.setLevel(logging.INFO)
+
+ serve(self, host=host, port=port, **options)
diff --git a/src/microdot_asyncio.py b/src/microdot_asyncio.py
index 6f1be73..c7c48f7 100644
--- a/src/microdot_asyncio.py
+++ b/src/microdot_asyncio.py
@@ -133,18 +133,27 @@ class Response(BaseResponse):
await stream.awrite(b'\r\n')
# body
+ async for body in self.body_iter():
+ await stream.awrite(body)
+
+ async def body_iter(self):
if self.body:
if hasattr(self.body, 'read'):
while True:
buf = self.body.read(self.send_file_buffer_size)
+ if _iscoroutine(buf): # pragma: no cover
+ buf = await buf
if len(buf):
- await stream.awrite(buf)
+ print('*', buf, self.send_file_buffer_size)
+ yield buf
if len(buf) < self.send_file_buffer_size:
break
if hasattr(self.body, 'close'): # pragma: no cover
- self.body.close()
+ result = self.body.close()
+ if _iscoroutine(result):
+ await result
else:
- await stream.awrite(self.body)
+ yield self.body
class Microdot(BaseMicrodot):
@@ -201,7 +210,7 @@ class Microdot(BaseMicrodot):
writer.awrite = MethodType(awrite, writer)
writer.aclose = MethodType(aclose, writer)
- await self.dispatch_request(reader, writer)
+ await self.handle_request(reader, writer)
if self.debug: # pragma: no cover
print('Starting async server on {host}:{port}...'.format(
@@ -251,13 +260,23 @@ class Microdot(BaseMicrodot):
def shutdown(self):
self.server.close()
- async def dispatch_request(self, reader, writer):
+ async def handle_request(self, reader, writer):
req = None
try:
req = await Request.create(self, reader,
writer.get_extra_info('peername'))
except Exception as exc: # pragma: no cover
print_exception(exc)
+
+ res = await self.dispatch_request(req)
+ await res.write(writer)
+ await writer.aclose()
+ if self.debug and req: # pragma: no cover
+ print('{method} {path} {status_code}'.format(
+ method=req.method, path=req.path,
+ status_code=res.status_code))
+
+ async def dispatch_request(self, req):
if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
@@ -310,12 +329,7 @@ class Microdot(BaseMicrodot):
res = Response(*res)
elif not isinstance(res, Response):
res = Response(res)
- await res.write(writer)
- await writer.aclose()
- if self.debug and req: # pragma: no cover
- print('{method} {path} {status_code}'.format(
- method=req.method, path=req.path,
- status_code=res.status_code))
+ return res
async def _invoke_handler(self, f_or_coro, *args, **kwargs):
ret = f_or_coro(*args, **kwargs)
diff --git a/src/microdot_wsgi.py b/src/microdot_wsgi.py
index 0d73c1c..d6ffbee 100644
--- a/src/microdot_wsgi.py
+++ b/src/microdot_wsgi.py
@@ -6,32 +6,29 @@ from microdot import Microdot as BaseMicrodot
from microdot import Request
-class WSGIRequest(Request):
- def __init__(self, app, environ):
- url = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
+class Microdot(BaseMicrodot):
+ def wsgi_app(self, environ, start_response):
+ path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
if 'QUERY_STRING' in environ and environ['QUERY_STRING']:
- url += '?' + environ['QUERY_STRING']
+ path += '?' + environ['QUERY_STRING']
headers = {}
for k, v in environ.items():
if k.startswith('HTTP_'):
h = '-'.join([p.title() for p in k[5:].split('_')])
headers[h] = v
- super().__init__(
- app,
+ req = Request(
+ self,
(environ['REMOTE_ADDR'], int(environ.get('REMOTE_PORT', '0'))),
environ['REQUEST_METHOD'],
- url,
+ path,
environ['SERVER_PROTOCOL'],
headers,
stream=environ['wsgi.input'])
- self.environ = environ
+ req.environ = environ
-
-class Microdot(BaseMicrodot):
- def wsgi_app(self, environ, start_response):
- req = WSGIRequest(self, environ)
res = self.dispatch_request(req)
res.complete()
+
reason = res.reason or ('OK' if res.status_code == 200 else 'N/A')
start_response(
str(res.status_code) + ' ' + reason,
diff --git a/tests/microdot_asgi/test_microdot_asgi.py b/tests/microdot_asgi/test_microdot_asgi.py
new file mode 100644
index 0000000..c620065
--- /dev/null
+++ b/tests/microdot_asgi/test_microdot_asgi.py
@@ -0,0 +1,133 @@
+import unittest
+import sys
+
+try:
+ from unittest import mock
+except ImportError:
+ mock = None
+
+from microdot_asgi import Microdot, Response
+from tests import mock_asyncio
+
+
+@unittest.skipIf(sys.implementation.name == 'micropython',
+ 'not supported under MicroPython')
+class TestMicrodotASGI(unittest.TestCase):
+ def test_asgi_request_with_query_string(self):
+ app = Microdot()
+
+ @app.post('/foo/bar')
+ async def index(req):
+ self.assertEqual(req.app, app)
+ self.assertEqual(req.client_addr, ('1.2.3.4', 1234))
+ self.assertEqual(req.method, 'POST')
+ self.assertEqual(req.http_version, 'HTTP/1.1')
+ self.assertEqual(req.path, '/foo/bar')
+ self.assertEqual(req.args, {'baz': ['1']})
+ self.assertEqual(req.cookies, {'session': 'xyz'})
+ self.assertEqual(req.body, b'body')
+
+ class R:
+ def __init__(self):
+ self.i = 0
+ self.body = [b're', b'sp', b'on', b'se', b'']
+
+ async def read(self, n):
+ data = self.body[self.i]
+ self.i += 1
+ return data
+
+ return Response(body=R(), headers={'Content-Length': '8'})
+
+ scope = {
+ 'type': 'http',
+ 'path': '/foo/bar',
+ 'query_string': b'baz=1',
+ 'headers': [('Authorization', 'Bearer 123'),
+ ('Cookie', 'session=xyz'),
+ ('Content-Length', 4)],
+ 'client': ['1.2.3.4', 1234],
+ 'method': 'POST',
+ 'http_version': '1.1',
+ }
+
+ async def receive():
+ return {
+ 'type': 'http.request',
+ 'body': b'body',
+ 'more_body': False,
+ }
+
+ async def send(packet):
+ if packet['type'] == 'http.response.start':
+ self.assertEqual(packet['status'], 200)
+ self.assertEqual(
+ packet['headers'],
+ [('Content-Length', '8'), ('Content-Type', 'text/plain')])
+ elif packet['type'] == 'http.response.body':
+ self.assertIn(packet['body'], [b're', b'sp', b'on', b'se'])
+
+ original_buffer_size = Response.send_file_buffer_size
+ Response.send_file_buffer_size = 2
+
+ mock_asyncio.run(app(scope, receive, send))
+
+ Response.send_file_buffer_size = original_buffer_size
+
+ def test_wsgi_request_without_query_string(self):
+ app = Microdot()
+
+ @app.route('/foo/bar')
+ async def index(req):
+ self.assertEqual(req.path, '/foo/bar')
+ self.assertEqual(req.args, {})
+ return 'response'
+
+ scope = {
+ 'type': 'http',
+ 'path': '/foo/bar',
+ 'headers': [('Authorization', 'Bearer 123'),
+ ('Cookie', 'session=xyz'),
+ ('Content-Length', 4)],
+ 'client': ['1.2.3.4', 1234],
+ 'method': 'POST',
+ 'http_version': '1.1',
+ }
+
+ async def receive():
+ return {
+ 'type': 'http.request',
+ 'body': b'body',
+ 'more_body': False,
+ }
+
+ async def send(packet):
+ pass
+
+ mock_asyncio.run(app(scope, receive, send))
+
+ def test_shutdown(self):
+ app = Microdot()
+
+ @app.route('/shutdown')
+ async def shutdown(request):
+ request.app.shutdown()
+
+ scope = {
+ 'type': 'http',
+ 'path': '/shutdown',
+ 'client': ['1.2.3.4', 1234],
+ 'method': 'GET',
+ 'http_version': '1.1',
+ }
+
+ async def receive():
+ pass
+
+ async def send(packet):
+ pass
+
+ with mock.patch('microdot_asgi.os.kill') as kill:
+ mock_asyncio.run(app(scope, receive, send))
+
+ kill.assert_called()
diff --git a/tests/microdot_wsgi/test_microdot_wsgi.py b/tests/microdot_wsgi/test_microdot_wsgi.py
index c8caf36..645a8ef 100644
--- a/tests/microdot_wsgi/test_microdot_wsgi.py
+++ b/tests/microdot_wsgi/test_microdot_wsgi.py
@@ -11,13 +11,27 @@ try:
except ImportError:
mock = None
-from microdot_wsgi import Microdot, WSGIRequest
+from microdot_wsgi import Microdot
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
class TestMicrodotWSGI(unittest.TestCase):
def test_wsgi_request_with_query_string(self):
+ app = Microdot()
+
+ @app.post('/foo/bar')
+ def index(req):
+ self.assertEqual(req.app, app)
+ self.assertEqual(req.client_addr, ('1.2.3.4', 1234))
+ self.assertEqual(req.method, 'POST')
+ self.assertEqual(req.http_version, 'HTTP/1.1')
+ self.assertEqual(req.path, '/foo/bar')
+ self.assertEqual(req.args, {'baz': ['1']})
+ self.assertEqual(req.cookies, {'session': 'xyz'})
+ self.assertEqual(req.body, b'body')
+ return 'response'
+
environ = {
'SCRIPT_NAME': '/foo',
'PATH_INFO': '/bar',
@@ -31,18 +45,25 @@ class TestMicrodotWSGI(unittest.TestCase):
'SERVER_PROTOCOL': 'HTTP/1.1',
'wsgi.input': io.BytesIO(b'body'),
}
- app = Microdot()
- req = WSGIRequest(app, environ)
- self.assertEqual(req.app, app)
- self.assertEqual(req.client_addr, ('1.2.3.4', 1234))
- self.assertEqual(req.method, 'POST')
- self.assertEqual(req.http_version, 'HTTP/1.1')
- self.assertEqual(req.path, '/foo/bar')
- self.assertEqual(req.args, {'baz': ['1']})
- self.assertEqual(req.cookies, {'session': 'xyz'})
- self.assertEqual(req.body, b'body')
- def test_wsgi_request_withiout_query_string(self):
+ def start_response(status, headers):
+ self.assertEqual(status, '200 OK')
+ self.assertEqual(
+ headers,
+ [('Content-Length', '8'), ('Content-Type', 'text/plain')])
+
+ r = app(environ, start_response)
+ self.assertEqual(next(r), b'response')
+
+ def test_wsgi_request_without_query_string(self):
+ app = Microdot()
+
+ @app.route('/foo/bar')
+ def index(req):
+ self.assertEqual(req.path, '/foo/bar')
+ self.assertEqual(req.args, {})
+ return 'response'
+
environ = {
'SCRIPT_NAME': '/foo',
'PATH_INFO': '/bar',
@@ -52,37 +73,11 @@ class TestMicrodotWSGI(unittest.TestCase):
'SERVER_PROTOCOL': 'HTTP/1.1',
'wsgi.input': io.BytesIO(b''),
}
- app = Microdot()
- req = WSGIRequest(app, environ)
- self.assertEqual(req.path, '/foo/bar')
- self.assertEqual(req.args, {})
-
- def test_wsgi_app(self):
- app = Microdot()
-
- @app.route('/foo')
- def foo(request):
- return 'bar'
-
- environ = {
- 'PATH_INFO': '/foo',
- 'REMOTE_ADDR': '1.2.3.4',
- 'REMOTE_PORT': '1234',
- 'REQUEST_METHOD': 'GET',
- 'SERVER_PROTOCOL': 'HTTP/1.1',
- 'wsgi.input': io.BytesIO(b''),
- }
def start_response(status, headers):
- self.assertEqual(status, '200 OK')
- self.assertEqual(headers, [('Content-Length', '3'),
- ('Content-Type', 'text/plain')])
+ pass
- res = app(environ, start_response)
- body = b''
- for b in res:
- body += b
- self.assertEqual(body, b'bar')
+ app(environ, start_response)
def test_shutdown(self):
app = Microdot()