6 _Request = collections.namedtuple(
27 class Request(_Request):
28 def __new__(cls, method, path, env=None):
34 accept = env.get('HTTP_ACCEPT')
35 accept_encoding = env.get('HTTP_ACCEPT_ENCODING')
36 accept_language = env.get('HTTP_ACCEPT_LANGUAGE')
37 content = env.get('CONTENT', '')
38 content_type = env.get('CONTENT_TYPE')
39 query = env.get('QUERY_STRING')
40 user_agent = env.get('HTTP_USER_AGENT')
42 content_length = env.get('CONTENT_LENGTH')
44 if content_length == '' or content_length is None:
48 content_length = int(content_length)
50 errors.append('Unable to parse Content-Length "{}"'.format(content_length))
54 cookie = http.cookies.SimpleCookie(env.get('HTTP_COOKIE'))
56 cookie = http.cookies.SimpleCookie()
59 GET = urllib.parse.parse_qs(query)
62 errors.append('Unable to parse GET parameters from query string "{}"'.format(query))
66 if content_type == 'application/x-www-form-urlencoded':
67 POST = urllib.parse.parse_qs(content)
70 errors.append('Unable to parse POST parameters from content string "{}"'.format(content))
74 errors.append('Unable to parse POST parameters from content string "{}"'.format(content))
81 elif method == 'POST':
86 result = super().__new__(
92 accept_encoding=accept_encoding,
93 accept_language=accept_language,
95 content_length = content_length,
96 content_type = content_type,
99 parameters=parameters,
102 user_agent=user_agent,
105 if path.startswith('/'):
106 result.subpath = path[1:]
108 result.subpath = path
112 def _get_request_from_env(env):
113 method = env.get('REQUEST_METHOD')
114 path = env.get('PATH_INFO')
115 return Request(method, path, env)
117 _Response = collections.namedtuple(
127 class Response(_Response):
128 def __new__(cls, content, **kwargs):
129 status = kwargs.pop('status', 200)
130 assert isinstance(status, int)
132 content_type = kwargs.pop('content_type')
133 assert isinstance(content_type, str)
135 extra_headers = kwargs.pop('extra_headers', ())
136 assert isinstance(extra_headers, tuple)
138 assert len(kwargs) == 0
140 return super().__new__(
143 content_type=content_type,
144 extra_headers=extra_headers,
151 ('Content-Type', self.content_type),
154 class HTMLResponse(Response):
155 def __new__(cls, content, **kwargs):
156 assert 'content_type' not in kwargs
158 return super().__new__(
161 content_type='text/html',
165 class JSONResponse(Response):
166 def __new__(cls, content_json, **kwargs):
167 assert 'content_type' not in kwargs
168 assert 'content' not in kwargs
170 self = super().__new__(
172 content=json.dumps(content_json),
173 content_type='application/json',
176 self.content_json = content_json
179 class TextResponse(Response):
180 def __new__(cls, content, **kwargs):
181 assert 'content_type' not in kwargs
183 return super().__new__(
186 content_type='text/plain',
190 _RedirectResponse = collections.namedtuple(
198 class RedirectResponse(_RedirectResponse):
199 def __new__(cls, location, **kwargs):
200 assert isinstance(location, str)
202 permanent = kwargs.pop('permanent', True)
203 assert isinstance(permanent, bool)
204 assert len(kwargs) == 0
206 return super().__new__(
214 return 308 if self.permanent else 307
218 return (('Location', self.location),)
224 def default_file_not_found_handler(request):
226 'Path "{}" with query "{}" not found'.format(request.path, request.query),
230 def route_on_subpath(**kwargs):
231 routes = kwargs.pop('routes')
232 file_not_found_handler = kwargs.pop(
233 'file_not_found_handler',
234 default_file_not_found_handler,
238 raise Exception('Keyword argument "routes" is required')
241 raise Exception('Unexpected keyword argument')
243 def wrapped(request):
244 split_subpath = request.subpath.split('/', 1)
245 subpath = split_subpath[0]
247 if len(split_subpath) == 2:
248 request.subpath = split_subpath[1]
252 return routes.get(subpath, file_not_found_handler)(request)
268 def default_method_not_allowed_handler(request):
271 def default_options_handler(handlers):
272 def handler(request):
273 return Response(','.join(handlers.keys()))
276 def route_on_method(**kwargs):
278 for method in REQUEST_METHODS:
280 handlers[method] = kwargs.pop(method)
282 method_not_allowed_handler = kwargs.pop(
283 'method_not_allowed',
284 default_method_not_allowed_handler,
287 assert len(kwargs) == 0
289 if 'OPTIONS' not in handlers:
290 handlers['OPTIONS'] = default_options_handler(handlers)
292 def handler(request):
294 request.method.upper(),
295 method_not_allowed_handler,
300 def _get_status(response):
303 307: '307 Temporary Redirect',
304 308: '308 Permanent Redirect',
307 def _get_headers(response):
308 return list(response.headers)
310 def _get_content(response):
311 content = response.content
313 if isinstance(content, bytes):
316 if isinstance(content, str):
317 return (content.encode('utf-8'),)
322 def app(env, start_fn):
323 response = handler(_get_request_from_env(env))
325 start_fn(_get_status(response), _get_headers(response))
326 return _get_content(response)