http_server_async: a bunch of changes

This commit is contained in:
Izalia Mae 2021-10-25 04:33:03 -04:00
parent dd2f3df431
commit 293a7ec107
5 changed files with 224 additions and 70 deletions

View file

@ -2,12 +2,13 @@ import asyncio, signal, socket, sys, time, traceback
from functools import partial
from http_router import Router, MethodNotAllowed, NotFound
from jinja2.exceptions import TemplateNotFound
from . import http_methods, error
from .config import Config
from .response import Response
#from .router import Router
from .view import Static
from .view import Static, Manifest, Robots, Style
from .. import logging
from ..dotdict import DotDict
@ -20,7 +21,7 @@ frontend = Path(__file__).resolve().parent.parent.join('http_frontend')
class Application:
def __init__(self, loop=None, **kwargs):
def __init__(self, loop=None, views=[], middleware=[], **kwargs):
self.loop = loop or asyncio.get_event_loop()
self._server = None
@ -29,25 +30,34 @@ class Application:
self.router = Router()
self.middleware = DotDict({'request': [], 'response': []})
self.template = Template(
self.cfg.tpl_search,
self.cfg.tpl_globals,
self.cfg.tpl_context,
self.cfg.tpl_autoescape
)
for view in views:
self.add_view(view)
self.template.add_env('app', self)
self.template.add_env('cfg', self.cfg)
self.template.add_env('len', len)
for mw in middleware:
self.add_middleware(mw)
if self.cfg.tpl_default:
self.template = Template(
self.cfg.tpl_search,
self.cfg.tpl_globals,
self.cfg.tpl_context,
self.cfg.tpl_autoescape
)
self.template.add_env('app', self)
self.template.add_env('cfg', self.cfg)
self.template.add_env('len', len)
self.template.add_search_path(frontend)
#self.add_view(view.Manifest)
#self.add_view(view.Robots)
#self.add_view(view.Style)
self.add_view(Manifest)
self.add_view(Robots)
self.add_view(Style)
self.add_static('/framework/favicon.ico', frontend.join('static/icon64.png'))
self.add_static('/framework/static/', frontend.join('static'))
else:
self.template = None
signal.signal(signal.SIGHUP, self.stop)
signal.signal(signal.SIGINT, self.stop)
signal.signal(signal.SIGQUIT, self.stop)
@ -83,25 +93,27 @@ class Application:
self.add_route(Static(src), path)
def add_middleware(self, handler):
def add_middleware(self, handler, attach=None):
if not asyncio.iscoroutinefunction(handler):
raise TypeError('Middleware handler must be a coroutine function or method')
try:
arg_len = len(handler.__code__.co_varnames)
if not attach:
try:
arg_len = handler.__code__.co_argcount
if arg_len == 1:
attach = 'request'
if arg_len == 1:
attach = 'request'
elif arg_len == 2:
attach = 'response'
elif arg_len == 2:
attach = 'response'
else:
raise TypeError(f'Middleware handler must have 1 (request) or 2 (response) arguments, not {arg_len}')
else:
raise TypeError(f'Middleware handler must have 1 (request) or 2 (response) arguments, not {arg_len}')
except Exception as e:
raise e from None
except Exception as e:
raise e from None
assert attach in ['request', 'response']
mwlist = self.middleware[attach]
if handler in mwlist:
@ -110,7 +122,7 @@ class Application:
mwlist.append(handler)
def render(self, template, *args, **kwargs):
def render(self, *args, **kwargs):
return self.template.render(*args, **kwargs)
@ -166,7 +178,7 @@ class Application:
try:
request = self.cfg.request_class(self, reader, writer.get_extra_info('peername')[0])
response = self.cfg.response_class()
response = self.cfg.response_class(request=request)
await request.parse_headers()
handler = self.get_route(request.path, request.method)
@ -189,22 +201,28 @@ class Application:
pass
else:
raise error.ServerError()
raise error.InternalServerError()
await self.handle_middleware(request, response)
except NotFound:
response = self.cfg.response_class.new_error('Not Found', 404)
response = self.cfg.response_class(request=request).set_error('Not Found', 404)
except MethodNotAllowed:
response = self.cfg.response_class.new_error('Method Not Allowed', 405)
response = self.cfg.response_class(request=request).set_error('Method Not Allowed', 405)
except error.RedirError as e:
response = self.cfg.response_class.new_redir(e.path, e.status)
except error.HttpError as e:
response = self.cfg.response_class.new_error(e.message, e.status)
response = self.cfg.response_class(request=request).set_error(e.message, e.status)
except TemplateNotFound as e:
response = self.cfg.response_class(request=request).set_error(f'Template not found: {e}', 500)
except:
traceback.print_exc()
response = self.cfg.response_class.new_error('Server Error', 500)
response = self.cfg.response_class(request=request).set_error('Server Error', 500)
try:
response.headers.update(self.cfg.default_headers)

View file

@ -1,3 +1,6 @@
from functools import partial
class HttpError(Exception):
def __init__(self, message, status=500):
super().__init__(f'HTTP Error {status}: {message}')
@ -6,31 +9,57 @@ class HttpError(Exception):
self.message = message
class Unauthorized(HttpError):
def __init__(self, message='Unauthorized'):
super().__init__(message, 401)
class RedirError(Exception):
def __init__(self, path, status=301):
super().__init__(f'HTTP Error {status}: {path}')
self.status = status
self.path = path
class Forbidden(HttpError):
def __init__(self, message='Forbidden'):
super().__init__(message, 403)
## 200 Errors
Ok = partial(HttpError, status=200)
Created = partial(HttpError,status=201)
Accepted = partial(HttpError,status=202)
NoContent = partial(HttpError,status=204)
ResetContent = partial(HttpError,status=205)
PartialContent = partial(HttpError,status=206)
## 300 Errors
NotModified = partial(HttpError, status=304)
class NotFound(HttpError):
def __init__(self, message='Not Found'):
super().__init__(message, 404)
## 400 Errors
BadRequest = partial(HttpError, status=400)
Unauthorized = partial(HttpError, status=401)
Forbidden = partial(HttpError, status=403)
NotFound = partial(HttpError, status=404)
MethodNotAllowed = partial(HttpError, status=405)
RequestTimeout = partial(HttpError,status=408)
Gone = partial(HttpError,status=410)
LengthRequired = partial(HttpError,status=411)
PreconditionFailed = partial(HttpError,status=412)
PayloadTooLarge = partial(HttpError,status=413)
UriTooLong = partial(HttpError,status=414)
UnsupportedMediaType = partial(HttpError,status=415)
RangeNotSatisfiable = partial(HttpError,status=416)
Teapot = partial(HttpError, status=418)
UpgradeRequired = partial(HttpError,status=426)
TooManyRequests = partial(HttpError,status=429)
RequestHeaderFieldsTooLarge = partial(HttpError,status=431)
UnavailableForLegalReasons = partial(HttpError,status=451)
## 500 Errors
InternalServerError = partial(HttpError, status=500)
NotImplemented = partial(HttpError,status=501)
BadGateway = partial(HttpError,status=502)
ServiceUnavailable = partial(HttpError,status=503)
GatewayTimeout = partial(HttpError,status=504)
HttpVersionNotSuported = partial(HttpError,status=505)
NetworkAuthenticationRequired = partial(HttpError,status=511)
class MethodNotAllowed(HttpError):
def __init__(self, message='Method Not Allowed'):
super().__init__(message, 405)
class Teapot(HttpError):
def __init__(self, message='I am a teapot'):
super().__init__(message, 418)
class ServerError(HttpError):
def __init__(self, message='ServerError'):
super().__init__(message, 500)
## Redirects
MovedPermanently = partial(RedirError, status=301)
Found = partial(RedirError, status=302)
SeeOther = partial(RedirError, status=303)
TemporaryRedirect = partial(RedirError, status=307)
PermanentRedirect = partial(RedirError, status=309)

View file

@ -13,7 +13,12 @@ LocalTime = datetime.now(UtcTime).astimezone().tzinfo
class Request:
__slots__ = ['_body', '_form', '_reader', 'app', 'address', 'method', 'path', 'version', 'headers', 'cookies', 'query', 'raw_query']
__slots__ = [
'_body', '_form', '_reader', 'app', 'address',
'method', 'path', 'version', 'headers', 'cookies',
'query', 'raw_query'
]
ctx = DotDict()
def __init__(self, app, reader, address):
@ -43,20 +48,19 @@ class Request:
self.ctx[key] = value
## These two functions are a mess and break everything
#def __getattr__(self, key):
#if key in self.__slots__:
#return super().__getattr__(self, key)
def __getattr__(self, key):
if key in self.__slots__:
return super().__getattr__(self, key)
#return self.ctx[key]
return self.ctx[key]
#def __setattr__(self, key, value):
#if key not in self.__slots__:
#self.ctx[key] = value
def __setattr__(self, key, value):
if key in self.__slots__:
super().__setattr__(key, value)
#else:
#super().__setattr__(key, value)
else:
self.ctx[key] = value
@property

View file

@ -1,4 +1,4 @@
import json
import json, traceback
from datetime import datetime
@ -8,8 +8,10 @@ from ..dotdict import MultiDotDict
class Response:
__slots__ = ['_body', 'headers', 'cookies', 'status']
def __init__(self, body=b'', status=200, headers={}, cookies={}, content_type='text/plain'):
__slots__ = ['_body', 'headers', 'cookies', 'status', 'request']
def __init__(self, body=b'', status=200, headers={}, cookies={}, content_type='text/plain', request=None):
self._body = b''
self.headers = Headers(headers)
@ -18,6 +20,7 @@ class Response:
self.body = body
self.status = status
self.content_type = content_type
self.request = request
@property
@ -78,19 +81,70 @@ class Response:
return response
def set_html(self, body=b''):
def set_text(self, body=b'', status=None):
self.body = body
if status:
self.status = status
return self
def set_html(self, body=b'', status=None):
self.content_type = 'text/html'
self.body = body
if status:
self.status = status
def set_json(self, body={}, activity=False):
return self
def set_template(self, template, context={}, content_type='text/html', status=None, request=None):
if not request:
request = self.request
if status:
self.status = status
self.body = request.app.render(template, context_data=context, request=request)
self.content_type = content_type
return self
def set_json(self, body={}, status=None, activity=False,):
self.content_type = 'application/activity+json' if activity else 'application/json'
self.body = body
if status:
self.status = status
return self
def set_redir(self, path, status=302):
self.headers['Location'] = path
self.status = status
return self
def set_error(self, message, status=500):
try:
return self.set_template('error.haml',
context = {
'error_message': message,
'response': self
},
status = status
)
except:
traceback.print_exc()
self.body = f'HTTP Error {status}: {message}'
self.status = status
return self
def compile(self):

View file

@ -53,3 +53,52 @@ def Static(src):
response.content_type = mime
return StaticHandler
### Frontend Template Views ###
class Manifest(View):
__path__ = '/framework/manifest.json'
async def get(self, request, response):
data = {
'name': self.cfg.name,
'short_name': self.cfg.name.replace(' ', ''),
'description': 'UvU',
'icons': [
{
'src': "/framework/static/icon512.png",
'sizes': '512x512',
'type': 'image/png'
},
{
'src': "/framework/static/icon64.png",
'sizes': '64x64',
'type': 'image/png'
}
],
'theme_color': str(response.default_theme.primary),
'background_color': str(response.default_theme.background),
'display': 'standalone',
'start_url': '/',
'scope': f'{self.cfg.proto}://{self.cfg.web_host}'
}
response.set_json(data)
class Robots(View):
__path__ = '/robots.txt'
async def get(self, request, response):
data = '# Disallow all\nUser-agent: *\nDisallow: /'
response.body = data
class Style(View):
__path__ = '/framework/style.css'
async def get(self, request, response):
response.body = self.app.render('base.css')
response.content_type = 'text/css'