forked from s3lph/matemat
Implemented a more explicit Pagelet return API using class instances to describe the action to take.
This commit is contained in:
parent
e3c65776b5
commit
1b00c80133
9 changed files with 186 additions and 75 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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('/')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
63
matemat/webserver/responses.py
Normal file
63
matemat/webserver/responses.py
Normal 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')
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
path: str,
|
||||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, 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]) -> 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')
|
||||
|
|
Loading…
Reference in a new issue