http_server_async: a bunch of changes
This commit is contained in:
parent
dd2f3df431
commit
293a7ec107
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Loading…
Reference in a new issue