WSGI support

This commit is contained in:
Miguel Grinberg
2022-05-25 00:03:30 +01:00
parent 0ca1e01e00
commit 1ae51ccdf7
6 changed files with 226 additions and 16 deletions

View File

@@ -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
View 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)

View File

@@ -25,3 +25,4 @@ package_dir =
py_modules =
microdot
microdot_asyncio
microdot_wsgi

View File

@@ -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
View 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)

View 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()