Implemented a more explicit Pagelet return API using class instances to describe the action to take.

This commit is contained in:
s3lph 2018-07-09 20:50:02 +02:00
parent e3c65776b5
commit 1b00c80133
9 changed files with 186 additions and 75 deletions

View file

@ -7,4 +7,5 @@ server will attempt to serve the request with a static resource in a previously
"""
from .requestargs import RequestArgument, RequestArguments
from .responses import PageletResponse, RedirectResponse, TemplateResponse
from .httpd import MatematWebserver, HttpHandler, pagelet

View file

@ -16,7 +16,7 @@ import jinja2
from matemat import __version__ as matemat_version
from matemat.exceptions import HttpException
from matemat.webserver import RequestArguments
from matemat.webserver import RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
from matemat.webserver.util import parse_args
#
@ -37,8 +37,7 @@ _PAGELET_PATHS: Dict[str, Callable[[str, # HTTP method (GET, POST, ...)
Dict[str, str]], # Response headers
Union[ # Return type: either a response body, or a redirect
bytes, str, # Response body: will assign HTTP/1.0 200 OK
Tuple[int, str], # Redirect: First element must be 301, second the redirect path
Tuple[str, Dict[str, Any]] # Jinja template name and kwargs
PageletResponse, # A generic response
]]] = dict()
# Inactivity timeout for client sessions
@ -67,9 +66,8 @@ def pagelet(path: str):
headers: The dictionary of HTTP response headers. Add headers you wish to send with the response.
returns: One of the following:
- A HTTP Response body as str or bytes
- A HTTP redirect: A tuple of 301 (an int) and the path to redirect to (a str)
- A Jinja template call: A tuple of the template name (a string) and the template rendering
arguments (a kwargs dict)
- A PageletResponse class instance: An instance of (a subclass of)
matemat.webserver.PageletResponse, e.g. encapsulating a redirect or a Jinja2 template.
raises: HttpException: If a non-200 HTTP status code should be returned
:param path: The path to register the function for.
@ -82,8 +80,7 @@ def pagelet(path: str):
Dict[str, str]],
Union[
bytes, str,
Tuple[int, str],
Tuple[str, Dict[str, Any]]
PageletResponse
]]):
# Add the function to the dict of pagelets
_PAGELET_PATHS[path] = fun
@ -221,8 +218,7 @@ class HttpHandler(BaseHTTPRequestHandler):
def _parse_pagelet_result(self,
pagelet_res: Union[bytes, # Response body as bytes
str, # Response body as str
Tuple[int, str], # Redirect
Tuple[str, Dict[str, Any]]], # Jinja template name, kwargs dict
PageletResponse], # Encapsulated or unresolved response body
headers: Dict[str, str]) \
-> Tuple[int, bytes]:
"""
@ -235,36 +231,34 @@ class HttpHandler(BaseHTTPRequestHandler):
"""
# The HTTP Response Status Code, defaults to 200 OK
hsc: int = 200
# The HTTP Response body, defaults to None
data: Union[bytes, str] = None
if isinstance(pagelet_res, tuple):
# If the return type is a tuple, it has to be either a redirect, in which case the first element must be
# int(301), or it is a template call, in which casse the first element must be the template name and the
# second element must be the kwargs dict to the template's render function
head, tail = pagelet_res
if head == 301 and isinstance(tail, str):
# Set the HTTP Response Status Code, and the redirect header
hsc = 301
headers['Location'] = tail
elif isinstance(head, str) and isinstance(tail, dict):
# Load the Jinja2 template and render it with the provided arguments
template = self.server.jinja_env.get_template(head)
tail['matemat_version'] = self.server_version
data = template.render(**tail)
else:
raise TypeError(f'Return value of pagelet not understood: {pagelet_res}')
elif isinstance(pagelet_res, str) or isinstance(pagelet_res, bytes):
# Return value is a response body
# The HTTP Response body, defaults to empty
data: bytes = bytes()
# If the response is a bytes object, it is used without further modification
if isinstance(pagelet_res, bytes):
data = pagelet_res
# If the response is a str object, it is encoded into a bytes object
elif isinstance(pagelet_res, str):
data = pagelet_res.encode('utf-8')
# If the response is a PageletResponse object, the status code is extracted. Generation of the body depends
# on the subtype
elif isinstance(pagelet_res, PageletResponse):
hsc = pagelet_res.status
# If the object is a RedirectResponse instance, no body is needed
if isinstance(pagelet_res, RedirectResponse):
headers['Location'] = pagelet_res.location
# If the object is a TemplateRespinse instance, pass the Jinja2 environment instance for rendering
elif isinstance(pagelet_res, TemplateResponse):
# noinspection PyProtectedMember
data = pagelet_res._render(self.server.jinja_env)
# else: Empty body
else:
# Return value is not a response body or a redirect
raise TypeError(f'Return value of pagelet not understood: {pagelet_res}')
# The pagelet may return None as data as a shorthand for an empty response
if data is None:
data = bytes()
# If the pagelet returns a Python str, convert it to an UTF-8 encoded bytes object
if isinstance(data, str):
data = data.encode('utf-8')
# Return the resulting status code and body
return hsc, data

View file

@ -1,8 +1,8 @@
from typing import Any, Dict, Tuple, Union
from typing import Any, Dict, Union
from matemat.exceptions import AuthenticationError, HttpException
from matemat.webserver import pagelet, RequestArguments
from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
from matemat.primitives import User
from matemat.db import MatematDatabase
@ -13,17 +13,17 @@ def login_page(method: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
-> Union[bytes, str, PageletResponse]:
if 'user' in session_vars:
return 301, '/'
return RedirectResponse('/')
if method == 'GET':
return 'login.html', {}
return TemplateResponse('login.html')
elif method == 'POST':
with MatematDatabase('test.db') as db:
try:
user: User = db.login(str(args.username), str(args.password))
except AuthenticationError:
return 301, '/login'
return RedirectResponse('/login')
session_vars['user'] = user
return 301, '/'
return RedirectResponse('/')
raise HttpException(405)

View file

@ -1,7 +1,7 @@
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, Union
from matemat.webserver import pagelet, RequestArguments
from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse
@pagelet('/logout')
@ -10,7 +10,7 @@ def logout(method: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
-> Union[bytes, str, PageletResponse]:
if 'user' in session_vars:
del session_vars['user']
return 301, '/'
return RedirectResponse('/')

View file

@ -1,7 +1,7 @@
from typing import Any, Dict, Optional, Tuple, Union
from typing import Any, Dict, Union
from matemat.webserver import pagelet, RequestArguments
from matemat.webserver import pagelet, RequestArguments, PageletResponse, TemplateResponse
from matemat.primitives import User
from matemat.db import MatematDatabase
@ -12,12 +12,12 @@ def main_page(method: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
-> Union[bytes, str, PageletResponse]:
with MatematDatabase('test.db') as db:
if 'user' in session_vars:
user: User = session_vars['user']
products = db.list_products()
return 'main.html', {'user': user, 'list': products}
return TemplateResponse('main.html', user=user, list=products)
else:
users = db.list_users()
return 'main.html', {'list': users}
return TemplateResponse('main.html', list=users)

View file

@ -4,7 +4,7 @@ from typing import Any, Dict, Tuple, Union
import urllib.parse
from matemat.exceptions import AuthenticationError, HttpException
from matemat.webserver import pagelet, RequestArguments
from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
from matemat.primitives import User
from matemat.db import MatematDatabase
@ -15,18 +15,18 @@ def touchkey_page(method: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str])\
-> Union[bytes, str, Tuple[int, str], Tuple[str, Dict[str, Any]]]:
-> Union[bytes, str, PageletResponse]:
if 'user' in session_vars:
return 301, '/'
return RedirectResponse('/')
if method == 'GET':
return 'touchkey.html', {'username': str(args.username)} if 'username' in args else {}
return TemplateResponse('touchkey.html', username=str(args.username) if 'username' in args else None)
elif method == 'POST':
with MatematDatabase('test.db') as db:
try:
user: User = db.login(str(args.username), touchkey=str(args.touchkey))
except AuthenticationError:
quoted = urllib.parse.quote_plus(bytes(args.username))
return 301, f'/touchkey?username={quoted}'
return RedirectResponse(f'/touchkey?username={quoted}')
session_vars['user'] = user
return 301, '/'
return RedirectResponse('/')
raise HttpException(405)

View file

@ -0,0 +1,63 @@
from jinja2 import Environment, Template
class PageletResponse:
"""
Base class for pagelet return values that require more action than simply sending plain data.
An instance of this base class will result in an empty 200 OK response.
"""
def __init__(self, status: int = 200):
"""
Create an empty response.
:param status: The HTTP status code, defaults to 200 (OK).
"""
self.status: int = status
class RedirectResponse(PageletResponse):
"""
A pagelet response that causes the server to redirect to another location, using a 301 Permanently Moved (uncached)
response status, and a Location header.
"""
def __init__(self, location: str):
"""
Create a redirection response with the given redirection location.
:param location: The location to redirect to.
"""
super().__init__(status=301)
self.location: str = location
class TemplateResponse(PageletResponse):
"""
A pagelet response that causes the server to load a Jinja2 template and render it with the provided arguments, then
sending the result as response body, with a 200 OK response status.
"""
def __init__(self, name: str, **kwargs):
"""
Create a template response with the given template name and arguments.
:param name: Name of the template to load.
:param kwargs: Arguments for rendering the template, will be passed to jinja2.Template.render as is.
"""
super().__init__()
self.name: str = name
self.kwargs = kwargs
def _render(self, jinja_env: Environment) -> bytes:
"""
Load and render the template using the Jinja2 environment managed by the web server instance. This method
should not be called by a pagelet.
:param jinja_env: The Jinja2 environment.
:return: An UTF-8 encoded bytes object containing the template rendering result.
"""
template: Template = jinja_env.get_template(self.name)
return template.render(**self.kwargs).encode('utf-8')

View file

@ -9,6 +9,8 @@ from abc import ABC
from datetime import datetime
from http.server import HTTPServer
import jinja2
from matemat.webserver import pagelet, RequestArguments
@ -107,6 +109,10 @@ class MockServer:
self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict()
# Webroot for statically served content
self.webroot: str = webroot
# Jinja environment with a single, static template
self.jinja_env = jinja2.Environment(
loader=jinja2.DictLoader({'test.txt': 'Hello, {{ what }}!'})
)
class MockSocket(bytes):

View file

@ -1,21 +1,31 @@
from typing import Any, Dict
from typing import Any, Dict, Union
import os
import os.path
from matemat.exceptions import HttpException
from matemat.webserver import HttpHandler, RequestArguments
from matemat.webserver import HttpHandler, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
@test_pagelet('/just/testing/serve_pagelet_ok')
def serve_test_pagelet_ok(method: str,
@test_pagelet('/just/testing/serve_pagelet_str')
def serve_test_pagelet_str(method: str,
path: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str]):
headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
headers['Content-Type'] = 'text/plain'
return 'serve test pagelet ok'
return 'serve test pagelet str'
@test_pagelet('/just/testing/serve_pagelet_bytes')
def serve_test_pagelet_bytes(method: str,
path: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
headers['Content-Type'] = 'application/octet-stream'
return b'serve\x80test\xffpagelet\xfebytes'
@test_pagelet('/just/testing/serve_pagelet_redirect')
@ -23,16 +33,27 @@ def serve_test_pagelet_redirect(method: str,
path: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str]):
return 301, '/foo/bar'
headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
return RedirectResponse('/foo/bar')
@test_pagelet('/just/testing/serve_pagelet_template')
def serve_test_pagelet_template(method: str,
path: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
headers['Content-Type'] = 'text/plain'
return TemplateResponse('test.txt', what='World')
# noinspection PyTypeChecker
@test_pagelet('/just/testing/serve_pagelet_fail')
def serve_test_pagelet_fail(method: str,
path: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str]):
headers: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
session_vars['test'] = 'hello, world!'
headers['Content-Type'] = 'text/plain'
raise HttpException()
@ -54,17 +75,29 @@ class TestServe(AbstractHttpdTest):
f.write('This should not be readable')
os.chmod(forbidden, 0)
def test_serve_pagelet_ok(self):
def test_serve_pagelet_str(self):
# Call the test pagelet that produces a 200 OK result
self.client_sock.set_request(b'GET /just/testing/serve_pagelet_ok HTTP/1.1\r\n\r\n')
self.client_sock.set_request(b'GET /just/testing/serve_pagelet_str HTTP/1.1\r\n\r\n')
HttpHandler(self.client_sock, ('::1', 45678), self.server)
packet = self.client_sock.get_response()
# Make sure the correct pagelet was called
self.assertEqual('serve_test_pagelet_ok', packet.pagelet)
self.assertEqual('serve_test_pagelet_str', packet.pagelet)
# Make sure the expected content is served
self.assertEqual(200, packet.statuscode)
self.assertEqual(b'serve test pagelet ok', packet.body)
self.assertEqual(b'serve test pagelet str', packet.body)
def test_serve_pagelet_bytes(self):
# Call the test pagelet that produces a 200 OK result
self.client_sock.set_request(b'GET /just/testing/serve_pagelet_bytes HTTP/1.1\r\n\r\n')
HttpHandler(self.client_sock, ('::1', 45678), self.server)
packet = self.client_sock.get_response()
# Make sure the correct pagelet was called
self.assertEqual('serve_test_pagelet_bytes', packet.pagelet)
# Make sure the expected content is served
self.assertEqual(200, packet.statuscode)
self.assertEqual(b'serve\x80test\xffpagelet\xfebytes', packet.body)
def test_serve_pagelet_fail(self):
# Call the test pagelet that produces a 500 Internal Server Error result
@ -81,12 +114,26 @@ class TestServe(AbstractHttpdTest):
HttpHandler(self.client_sock, ('::1', 45678), self.server)
packet = self.client_sock.get_response()
# Make sure the correct pagelet was called
self.assertEqual('serve_test_pagelet_redirect', packet.pagelet)
# Make sure the correct redirect is issued
self.assertEqual(301, packet.statuscode)
self.assertEqual('/foo/bar', packet.headers['Location'])
# Make sure the response body is empty
self.assertEqual(0, len(packet.body))
def test_serve_pagelet_template(self):
# Call the test pagelet that redirects to another path
self.client_sock.set_request(b'GET /just/testing/serve_pagelet_template HTTP/1.1\r\n\r\n')
HttpHandler(self.client_sock, ('::1', 45678), self.server)
packet = self.client_sock.get_response()
# Make sure the correct pagelet was called
self.assertEqual('serve_test_pagelet_template', packet.pagelet)
self.assertEqual(200, packet.statuscode)
# Make sure the response body was rendered correctly by the templating engine
self.assertEqual(b'Hello, World!', packet.body)
def test_serve_static_ok(self):
# Request a static resource
self.client_sock.set_request(b'GET /static_resource.txt HTTP/1.1\r\n\r\n')