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

View file

@ -1,3 +1,6 @@
from functools import partial
class HttpError(Exception): class HttpError(Exception):
def __init__(self, message, status=500): def __init__(self, message, status=500):
super().__init__(f'HTTP Error {status}: {message}') super().__init__(f'HTTP Error {status}: {message}')
@ -6,31 +9,57 @@ class HttpError(Exception):
self.message = message self.message = message
class Unauthorized(HttpError): class RedirError(Exception):
def __init__(self, message='Unauthorized'): def __init__(self, path, status=301):
super().__init__(message, 401) super().__init__(f'HTTP Error {status}: {path}')
self.status = status
self.path = path
class Forbidden(HttpError): ## 200 Errors
def __init__(self, message='Forbidden'): Ok = partial(HttpError, status=200)
super().__init__(message, 403) 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): ## 400 Errors
def __init__(self, message='Not Found'): BadRequest = partial(HttpError, status=400)
super().__init__(message, 404) 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): ## Redirects
def __init__(self, message='Method Not Allowed'): MovedPermanently = partial(RedirError, status=301)
super().__init__(message, 405) Found = partial(RedirError, status=302)
SeeOther = partial(RedirError, status=303)
TemporaryRedirect = partial(RedirError, status=307)
class Teapot(HttpError): PermanentRedirect = partial(RedirError, status=309)
def __init__(self, message='I am a teapot'):
super().__init__(message, 418)
class ServerError(HttpError):
def __init__(self, message='ServerError'):
super().__init__(message, 500)

View file

@ -13,7 +13,12 @@ LocalTime = datetime.now(UtcTime).astimezone().tzinfo
class Request: 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() ctx = DotDict()
def __init__(self, app, reader, address): def __init__(self, app, reader, address):
@ -43,20 +48,19 @@ class Request:
self.ctx[key] = value self.ctx[key] = value
## These two functions are a mess and break everything def __getattr__(self, key):
#def __getattr__(self, key): if key in self.__slots__:
#if key in self.__slots__: return super().__getattr__(self, key)
#return super().__getattr__(self, key)
#return self.ctx[key] return self.ctx[key]
#def __setattr__(self, key, value): def __setattr__(self, key, value):
#if key not in self.__slots__: if key in self.__slots__:
#self.ctx[key] = value super().__setattr__(key, value)
#else: else:
#super().__setattr__(key, value) self.ctx[key] = value
@property @property

View file

@ -1,4 +1,4 @@
import json import json, traceback
from datetime import datetime from datetime import datetime
@ -8,8 +8,10 @@ from ..dotdict import MultiDotDict
class Response: class Response:
__slots__ = ['_body', 'headers', 'cookies', 'status'] __slots__ = ['_body', 'headers', 'cookies', 'status', 'request']
def __init__(self, body=b'', status=200, headers={}, cookies={}, content_type='text/plain'):
def __init__(self, body=b'', status=200, headers={}, cookies={}, content_type='text/plain', request=None):
self._body = b'' self._body = b''
self.headers = Headers(headers) self.headers = Headers(headers)
@ -18,6 +20,7 @@ class Response:
self.body = body self.body = body
self.status = status self.status = status
self.content_type = content_type self.content_type = content_type
self.request = request
@property @property
@ -78,19 +81,70 @@ class Response:
return 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.content_type = 'text/html'
self.body = body 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.content_type = 'application/activity+json' if activity else 'application/json'
self.body = body 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): 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.body = f'HTTP Error {status}: {message}'
self.status = status self.status = status
return self
def compile(self): def compile(self):

View file

@ -53,3 +53,52 @@ def Static(src):
response.content_type = mime response.content_type = mime
return StaticHandler 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'