From 293a7ec107718d19a6f764b07f37df1df792c5e1 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 25 Oct 2021 04:33:03 -0400 Subject: [PATCH] http_server_async: a bunch of changes --- izzylib/http_server_async/application.py | 82 +++++++++++++++--------- izzylib/http_server_async/error.py | 73 ++++++++++++++------- izzylib/http_server_async/request.py | 26 ++++---- izzylib/http_server_async/response.py | 64 ++++++++++++++++-- izzylib/http_server_async/view.py | 49 ++++++++++++++ 5 files changed, 224 insertions(+), 70 deletions(-) diff --git a/izzylib/http_server_async/application.py b/izzylib/http_server_async/application.py index 2e4c134..2843a6c 100644 --- a/izzylib/http_server_async/application.py +++ b/izzylib/http_server_async/application.py @@ -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) diff --git a/izzylib/http_server_async/error.py b/izzylib/http_server_async/error.py index a00920f..feb5388 100644 --- a/izzylib/http_server_async/error.py +++ b/izzylib/http_server_async/error.py @@ -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) diff --git a/izzylib/http_server_async/request.py b/izzylib/http_server_async/request.py index dac9646..1ca10d8 100644 --- a/izzylib/http_server_async/request.py +++ b/izzylib/http_server_async/request.py @@ -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 diff --git a/izzylib/http_server_async/response.py b/izzylib/http_server_async/response.py index c8fbcfa..2c289e3 100644 --- a/izzylib/http_server_async/response.py +++ b/izzylib/http_server_async/response.py @@ -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): diff --git a/izzylib/http_server_async/view.py b/izzylib/http_server_async/view.py index 646a68f..67b4418 100644 --- a/izzylib/http_server_async/view.py +++ b/izzylib/http_server_async/view.py @@ -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'