There are many really nice webframeworks written by Python in this world.
You know these webframeworks on WSGI, so let's building your original webframework now!
If you want to know the detail about WSGI spec, please reference PEP3333. This post doesn't care any details, just implementing.
Write our application
Luckly, you have already wsgi server defined at wsgiref
module. All you need is creating an WSGI application entrypoint.
A most simple application is like below.
# app.py
from wsgiref.simple_server import make_server
def app(environ, start_response):
start_response('200 OK', headers=[('Content-Type', 'text/plain:charset=utf-8')])
response = [b'my toy app']
return response
with make_server('', 8088, app) as server:
server.serve_forever()
Hmm, this is so boring. Typically an webframework must have http request handlers and routing systems. So I should have these.
At first, I should think about the way to using in the real world.
from wsgiref.simple_server import make_server
things = {x: x.upper() for x in ('a', 'b', 'c')}
def get_all_things(request):
return ','.join(things.keys())
def create_things(request):
# do something
return 'ok'
class App:
def get(self, path, handler):
# TODO
return self
def post(self, path, handler):
# TODO
return self
def __call__(self, environment, start_response):
# TODO
return response
def start(self):
with make_server('', 8088, self) as server:
server.serve_forever()
app = App().get('/things', get_all_things).post('/things', create_things)
app.start()
This webframework has
- Routing
- Querystring
- Path parameter
- Only GET and POST handlers
- App class includes WSGI server starter
and doesn't have
- Middleware
- Any error handlings
- Staticfile serving
- Template
- Flexible content type
- Redirect
- More, more, more..
Overview
This is implementing example.
from urllib.parse import parse_qs
from collections import defaultdict
import re
from wsgiref.simple_server import make_server
class App:
handler_map = defaultdict(dict)
def _register(self, method_name, path, handler):
self.handler_map[fr'^{path}$'].update({method_name: handler})
return self
def get(self, path, handler):
return self._register('get', path, handler)
def post(self, path, handler):
return self._register('post', path, handler)
def _find_handler(self, path, method):
for pattern, info in self.handler_map.items():
matched = re.match(pattern, path)
if matched:
return info[method], matched.groupdict()
def __call__(self, environment, start_response):
method = environment['REQUEST_METHOD'].lower()
path = environment['PATH_INFO']
status = {
'get': '200 OK',
'post': '201 Created',
}[method]
start_response(status, headers=[('Content-Type', 'text/plain:charset=utf-8')])
request_body = {
'get': lambda: environment['QUERY_STRING'],
'post': lambda: environment['wsgi.input'].read(int(environment.get('CONTENT_LENGTH', 0))).decode(),
}[method]
handler, path_params = self._find_handler(path, method)
response = handler(parse_qs(request_body), **path_params)
return [response.encode()]
def start(self):
with make_server('', 8088, self) as server:
server.serve_forever()
Routing paths and mapping handlers are important things.
App
memories routing paths and corresponding handlers in handler_map
.
See _register
.
def _register(self, method_name, path, handler):
self.handler_map[fr'^{path}$'].update({method_name: handler})
return self
def get(self, path, handler):
return self._register('get', path, handler)
def post(self, path, handler):
return self._register('post', path, handler)
This is a just method wrapper against each http method.
So App
retrieves the handler corresponding requested path.
I choice re
module to detect path like Django etc.
Handling request is in __call__
.
def __call__(self, environment, start_response):
method = environment['REQUEST_METHOD'].lower()
path = environment['PATH_INFO']
status = {
'get': '200 OK',
'post': '201 Created',
}[method]
start_response(status, headers=[('Content-Type', 'text/plain:charset=utf-8')])
request_body = {
'get': lambda: environment['QUERY_STRING'],
'post': lambda: environment['wsgi.input'].read(int(environment.get('CONTENT_LENGTH', 0))).decode(),
}[method]()
handler, path_params = self._find_handler(path, method)
response = handler(parse_qs(request_body), **path_params)
return [response.encode()]
__call__
does just simple processes.
- fetches an http method
- fetches a request path
- builds a request body corresponding the http method
- fetches a handler
- calls a handler
OK, now I get my own toy webframework! Try to run.
Start server.
$ python3 app.py
And request.
$ curl localhost:8088
$ curl localhost:8088/things
$ curl localhost:8088/things/a
$ curl -X POST -d 'x=X' localhost:8088/things
At last, I created tiny toy not productive webframework named tomoyo which is a little little more complex than above webframework.
Bye!
Top comments (1)
nice article !