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 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue