WSGI support
This commit is contained in:
@@ -20,7 +20,7 @@ htmldoc = '''<!DOCTYPE html>
|
||||
|
||||
@app.route('/')
|
||||
def hello(request):
|
||||
return Response(body=htmldoc, headers={'Content-Type': 'text/html'})
|
||||
return htmldoc, 200, {'Content-Type': 'text/html'}
|
||||
|
||||
|
||||
@app.route('/shutdown')
|
||||
|
||||
33
examples/hello_wsgi.py
Normal file
33
examples/hello_wsgi.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from microdot_wsgi import Microdot, Response
|
||||
|
||||
app = Microdot()
|
||||
|
||||
htmldoc = '''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Microdot Example Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>Microdot Example Page</h1>
|
||||
<p>Hello from Microdot!</p>
|
||||
<p><a href="/shutdown">Click to shutdown the server</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def hello(request):
|
||||
return htmldoc, 200, {'Content-Type': 'text/html'}
|
||||
|
||||
|
||||
@app.route('/shutdown')
|
||||
def shutdown(request):
|
||||
request.app.shutdown()
|
||||
return 'The server is shutting down...'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
@@ -25,3 +25,4 @@ package_dir =
|
||||
py_modules =
|
||||
microdot
|
||||
microdot_asyncio
|
||||
microdot_wsgi
|
||||
|
||||
@@ -440,20 +440,22 @@ class Response():
|
||||
stream.write('{header}: {value}\r\n'.format(
|
||||
header=header, value=value).encode())
|
||||
stream.write(b'\r\n')
|
||||
for body in self.body_iter():
|
||||
stream.write(body)
|
||||
|
||||
# body
|
||||
def body_iter(self):
|
||||
if self.body:
|
||||
if hasattr(self.body, 'read'):
|
||||
while True:
|
||||
buf = self.body.read(self.send_file_buffer_size)
|
||||
if len(buf):
|
||||
stream.write(buf)
|
||||
yield buf
|
||||
if len(buf) < self.send_file_buffer_size:
|
||||
break
|
||||
if hasattr(self.body, 'close'): # pragma: no cover
|
||||
self.body.close()
|
||||
else:
|
||||
stream.write(self.body)
|
||||
yield self.body
|
||||
|
||||
@classmethod
|
||||
def redirect(cls, location, status_code=302):
|
||||
@@ -800,7 +802,7 @@ class Microdot():
|
||||
break
|
||||
else:
|
||||
raise
|
||||
create_thread(self.dispatch_request, sock, addr)
|
||||
create_thread(self.handle_request, sock, addr)
|
||||
|
||||
def shutdown(self):
|
||||
"""Request a server shutdown. The server will then exit its request
|
||||
@@ -827,7 +829,7 @@ class Microdot():
|
||||
break
|
||||
return f
|
||||
|
||||
def dispatch_request(self, sock, addr):
|
||||
def handle_request(self, sock, addr):
|
||||
if not hasattr(sock, 'readline'): # pragma: no cover
|
||||
stream = sock.makefile("rwb")
|
||||
else:
|
||||
@@ -838,6 +840,19 @@ class Microdot():
|
||||
req = Request.create(self, stream, addr)
|
||||
except Exception as exc: # pragma: no cover
|
||||
print_exception(exc)
|
||||
res = self.dispatch_request(req)
|
||||
res.write(stream)
|
||||
stream.close()
|
||||
if stream != sock: # pragma: no cover
|
||||
sock.close()
|
||||
if self.shutdown_requested: # pragma: no cover
|
||||
self.server.close()
|
||||
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))
|
||||
|
||||
def dispatch_request(self, req):
|
||||
if req:
|
||||
if req.content_length > req.max_content_length:
|
||||
if 413 in self.error_handlers:
|
||||
@@ -884,16 +899,7 @@ class Microdot():
|
||||
res = Response(*res)
|
||||
elif not isinstance(res, Response):
|
||||
res = Response(res)
|
||||
res.write(stream)
|
||||
stream.close()
|
||||
if stream != sock: # pragma: no cover
|
||||
sock.close()
|
||||
if self.shutdown_requested: # pragma: no cover
|
||||
self.server.close()
|
||||
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
|
||||
|
||||
|
||||
redirect = Response.redirect
|
||||
|
||||
61
src/microdot_wsgi.py
Normal file
61
src/microdot_wsgi.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
from microdot import * # noqa: F401, F403
|
||||
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', '')
|
||||
if 'QUERY_STRING' in environ and environ['QUERY_STRING']:
|
||||
url += '?' + 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,
|
||||
(environ['REMOTE_ADDR'], int(environ.get('REMOTE_PORT', '0'))),
|
||||
environ['REQUEST_METHOD'],
|
||||
url,
|
||||
environ['SERVER_PROTOCOL'],
|
||||
headers,
|
||||
stream=environ['wsgi.input'])
|
||||
self.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,
|
||||
[(name, value) for name, value in res.headers.items()])
|
||||
return res.body_iter()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgi_app(environ, start_response)
|
||||
|
||||
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)
|
||||
109
tests/microdot_wsgi/test_microdot_wsgi.py
Normal file
109
tests/microdot_wsgi/test_microdot_wsgi.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
try:
|
||||
import uio as io
|
||||
except ImportError:
|
||||
import io
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
mock = None
|
||||
|
||||
from microdot_wsgi import Microdot, WSGIRequest
|
||||
|
||||
|
||||
@unittest.skipIf(sys.implementation.name == 'micropython',
|
||||
'not supported under MicroPython')
|
||||
class TestMicrodotWSGI(unittest.TestCase):
|
||||
def test_wsgi_request_with_query_string(self):
|
||||
environ = {
|
||||
'SCRIPT_NAME': '/foo',
|
||||
'PATH_INFO': '/bar',
|
||||
'QUERY_STRING': 'baz=1',
|
||||
'HTTP_AUTHORIZATION': 'Bearer 123',
|
||||
'HTTP_COOKIE': 'session=xyz',
|
||||
'HTTP_CONTENT_LENGTH': '4',
|
||||
'REMOTE_ADDR': '1.2.3.4',
|
||||
'REMOTE_PORT': '1234',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
'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):
|
||||
environ = {
|
||||
'SCRIPT_NAME': '/foo',
|
||||
'PATH_INFO': '/bar',
|
||||
'REMOTE_ADDR': '1.2.3.4',
|
||||
'REMOTE_PORT': '1234',
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'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')])
|
||||
|
||||
res = app(environ, start_response)
|
||||
body = b''
|
||||
for b in res:
|
||||
body += b
|
||||
self.assertEqual(body, b'bar')
|
||||
|
||||
def test_shutdown(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/shutdown')
|
||||
def shutdown(request):
|
||||
request.app.shutdown()
|
||||
|
||||
environ = {
|
||||
'PATH_INFO': '/shutdown',
|
||||
'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):
|
||||
pass
|
||||
|
||||
with mock.patch('microdot_wsgi.os.kill') as kill:
|
||||
app(environ, start_response)
|
||||
|
||||
kill.assert_called()
|
||||
Reference in New Issue
Block a user