api changes to dotdict/path and new http_server_async submodule
This commit is contained in:
parent
29b5252e3e
commit
3e1cc946fd
|
@ -19,14 +19,16 @@ from .path import Path
|
||||||
from .dotdict import DotDict, LowerDotDict, DefaultDotDict, MultiDotDict, JsonEncoder
|
from .dotdict import DotDict, LowerDotDict, DefaultDotDict, MultiDotDict, JsonEncoder
|
||||||
from .misc import *
|
from .misc import *
|
||||||
from .cache import CacheDecorator, LruCache, TtlCache
|
from .cache import CacheDecorator, LruCache, TtlCache
|
||||||
|
from .config import BaseConfig, JsonConfig, YamlConfig
|
||||||
from .connection import Connection
|
from .connection import Connection
|
||||||
|
|
||||||
from .http_client import HttpClient, HttpResponse
|
from .http_client import HttpClient, HttpResponse
|
||||||
|
|
||||||
|
Config = JsonConfig
|
||||||
|
|
||||||
|
|
||||||
def log_import_error(package, *message):
|
def log_import_error(package, *message):
|
||||||
izzylog.debug(*message)
|
izzylog.debug(*message)
|
||||||
path = Path(__file__).resolve.parent.join(package)
|
path = Path(__file__).resolve().parent.join(package)
|
||||||
|
|
||||||
if path.exists and izzylog.get_config('level') == logging.Levels.DEBUG:
|
if path.exists and izzylog.get_config('level') == logging.Levels.DEBUG:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
|
@ -118,21 +118,24 @@ class LowerDotDict(DotDict):
|
||||||
return super().__setitem__(key.lower(), value)
|
return super().__setitem__(key.lower(), value)
|
||||||
|
|
||||||
|
|
||||||
class MultiDotDict(DotDict):
|
class MultiDotDict(LowerDotDict):
|
||||||
def __getattr__(self, key):
|
def __setitem__(self, key, value, single=False):
|
||||||
return self.__getitem__(key)
|
key = key.lower()
|
||||||
|
|
||||||
|
if single:
|
||||||
|
super().__setitem__(key, [value])
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
else:
|
||||||
try:
|
try:
|
||||||
self.__getitem__(key.lower(), False).append(value)
|
self.__getitem__(key, False).append(value)
|
||||||
|
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
super().__setitem__(key.lower(), [value])
|
super().__setitem__(key, [value])
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, key, single=True):
|
def __getitem__(self, key, single=True):
|
||||||
values = super().__getitem__(key.lower())
|
key = key.lower()
|
||||||
|
values = super().__getitem__(key)
|
||||||
|
|
||||||
if single:
|
if single:
|
||||||
try:
|
try:
|
||||||
|
@ -169,10 +172,7 @@ class MultiDotDict(DotDict):
|
||||||
|
|
||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key, value):
|
||||||
if self.get(key):
|
self.__setitem__(key, value, True)
|
||||||
del self[key]
|
|
||||||
|
|
||||||
self[key] = value
|
|
||||||
|
|
||||||
|
|
||||||
def delone(self, key, value):
|
def delone(self, key, value):
|
||||||
|
@ -180,7 +180,7 @@ class MultiDotDict(DotDict):
|
||||||
|
|
||||||
|
|
||||||
def delete(self, key):
|
def delete(self, key):
|
||||||
self.pop(key)
|
del self[key]
|
||||||
|
|
||||||
|
|
||||||
class JsonEncoder(json.JSONEncoder):
|
class JsonEncoder(json.JSONEncoder):
|
||||||
|
|
|
@ -8,3 +8,15 @@ class DBusServerError(Exception):
|
||||||
|
|
||||||
class HttpFileDownloadedError(Exception):
|
class HttpFileDownloadedError(Exception):
|
||||||
'raise when a download failed for any reason'
|
'raise when a download failed for any reason'
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMethodException(Exception):
|
||||||
|
def __init__(self, method):
|
||||||
|
super().__init__(f'Invalid HTTP method: {method}')
|
||||||
|
self.method = method
|
||||||
|
|
||||||
|
|
||||||
|
class MethodNotHandledException(Exception):
|
||||||
|
def __init__(self, method):
|
||||||
|
super().__init__(f'HTTP method not handled by handler: {method}')
|
||||||
|
self.method = method
|
||||||
|
|
|
@ -29,7 +29,7 @@ log_ext_ignore = [
|
||||||
'divx', 'mov', 'mp4', 'webm', 'wmv'
|
'divx', 'mov', 'mp4', 'webm', 'wmv'
|
||||||
]
|
]
|
||||||
|
|
||||||
frontend = Path(__file__).resolve.parent.join('frontend')
|
frontend = Path(__file__).resolve().parent.join('frontend')
|
||||||
|
|
||||||
class Application(sanic.Sanic):
|
class Application(sanic.Sanic):
|
||||||
_extra = DotDict()
|
_extra = DotDict()
|
||||||
|
@ -68,8 +68,9 @@ class Application(sanic.Sanic):
|
||||||
self.add_error_handler(MissingTemplateError)
|
self.add_error_handler(MissingTemplateError)
|
||||||
self.add_error_handler(GenericError)
|
self.add_error_handler(GenericError)
|
||||||
|
|
||||||
## compat
|
for sig in signal.valid_signals():
|
||||||
self.start = self.run
|
if type(sig) != int:
|
||||||
|
self.set_signal(sig)
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
|
@ -138,12 +139,7 @@ class Application(sanic.Sanic):
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
|
||||||
def run(self, log=False, async_server=False):
|
def start(self, log=False, async_server=False):
|
||||||
signal.signal(signal.SIGHUP, self.finish)
|
|
||||||
signal.signal(signal.SIGINT, self.finish)
|
|
||||||
signal.signal(signal.SIGQUIT, self.finish)
|
|
||||||
signal.signal(signal.SIGTERM, self.finish)
|
|
||||||
|
|
||||||
# register built-in middleware now so they're last in the chain
|
# register built-in middleware now so they're last in the chain
|
||||||
self.add_middleware(Headers)
|
self.add_middleware(Headers)
|
||||||
|
|
||||||
|
@ -167,7 +163,7 @@ class Application(sanic.Sanic):
|
||||||
return_asyncio_server = True
|
return_asyncio_server = True
|
||||||
)
|
)
|
||||||
|
|
||||||
super().run(
|
self.run(
|
||||||
host = self.cfg.listen,
|
host = self.cfg.listen,
|
||||||
port = self.cfg.port,
|
port = self.cfg.port,
|
||||||
workers = self.cfg.workers,
|
workers = self.cfg.workers,
|
||||||
|
@ -181,15 +177,41 @@ class Application(sanic.Sanic):
|
||||||
return self.run(async_server=True)
|
return self.run(async_server=True)
|
||||||
|
|
||||||
|
|
||||||
def finish(self, *args):
|
def set_signal(self, sig):
|
||||||
|
if type(sig) == int:
|
||||||
|
return
|
||||||
|
|
||||||
|
if sig in [signal.SIGKILL]:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
signal.signal(sig, partial(self.finish, sig))
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def finish(self, sig, *args):
|
||||||
if self.cfg.sig_handler:
|
if self.cfg.sig_handler:
|
||||||
self.cfg.sig_handler(self, *self.cfg.sig_handler_args, **self.cfg.sig_handler_kwargs)
|
self.cfg.sig_handler(self, sig, *self.cfg.sig_handler_args, **self.cfg.sig_handler_kwargs)
|
||||||
|
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.loop.stop()
|
||||||
|
self.loop.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exception(e)
|
||||||
|
|
||||||
#izzylog.info('Bye! :3')
|
#izzylog.info('Bye! :3')
|
||||||
#sys.exit()
|
#sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
#async def dispatch(self, *args, **kwargs):
|
||||||
|
#return
|
||||||
|
|
||||||
|
|
||||||
def parse_level(level):
|
def parse_level(level):
|
||||||
if type(level) == int:
|
if type(level) == int:
|
||||||
level = UserLevel(level)
|
level = UserLevel(level)
|
||||||
|
|
10
izzylib/http_server_async/__init__.py
Normal file
10
izzylib/http_server_async/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
http_methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']
|
||||||
|
|
||||||
|
|
||||||
|
from .application import Application
|
||||||
|
from .cookies import Cookies
|
||||||
|
from .middleware import Middleware
|
||||||
|
from .request import Request
|
||||||
|
from .response import Response
|
||||||
|
from .router import Router
|
||||||
|
from .view import View
|
196
izzylib/http_server_async/application.py
Normal file
196
izzylib/http_server_async/application.py
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
import asyncio, signal, socket, sys, time, traceback
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
from http_router import Router, MethodNotAllowed, NotFound
|
||||||
|
|
||||||
|
from . import http_methods, error
|
||||||
|
from .config import Config
|
||||||
|
from .response import Response
|
||||||
|
#from .router import Router
|
||||||
|
|
||||||
|
from .. import logging
|
||||||
|
from ..dotdict import DotDict
|
||||||
|
from ..exceptions import MethodNotHandledException
|
||||||
|
|
||||||
|
|
||||||
|
class Application:
|
||||||
|
def __init__(self, loop=None, **kwargs):
|
||||||
|
self.loop = loop or asyncio.get_event_loop()
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
self.cfg = Config(**kwargs)
|
||||||
|
#self.router = Router(trim_last_slash=True)
|
||||||
|
self.router = Router()
|
||||||
|
self.middleware = DotDict({'request': [], 'response': []})
|
||||||
|
|
||||||
|
#self.add_view = self.router.add_view
|
||||||
|
#self.add_route = self.router.add_route
|
||||||
|
#self.add_static = self.router.add_static
|
||||||
|
#self.get_route = self.router.get_route
|
||||||
|
|
||||||
|
signal.signal(signal.SIGHUP, self.stop)
|
||||||
|
signal.signal(signal.SIGINT, self.stop)
|
||||||
|
signal.signal(signal.SIGQUIT, self.stop)
|
||||||
|
signal.signal(signal.SIGTERM, self.stop)
|
||||||
|
|
||||||
|
|
||||||
|
def get_route(self, path, method='GET'):
|
||||||
|
return self.router(path, method.upper())
|
||||||
|
|
||||||
|
|
||||||
|
def add_route(self, handler, path, method='GET'):
|
||||||
|
self.router.bind(handler, path, methods=[method.upper()])
|
||||||
|
|
||||||
|
|
||||||
|
def add_view(self, view):
|
||||||
|
paths = view.__path__ if isinstance(view.__path__, list) else [view.__path__]
|
||||||
|
|
||||||
|
view_class = view(self)
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
for method in http_methods:
|
||||||
|
try:
|
||||||
|
self.add_route(view_class.get_handler(method), path, method)
|
||||||
|
|
||||||
|
except MethodNotHandledException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def add_middleware(self, handler):
|
||||||
|
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 arg_len == 1:
|
||||||
|
attach = 'request'
|
||||||
|
|
||||||
|
elif arg_len == 2:
|
||||||
|
attach = 'response'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
mwlist = self.middleware[attach]
|
||||||
|
|
||||||
|
if handler in mwlist:
|
||||||
|
return logging.error(f'Middleware handler already added to {attach}: {handler}')
|
||||||
|
|
||||||
|
mwlist.append(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def stop(self, *_):
|
||||||
|
if not self._server:
|
||||||
|
print('server not running')
|
||||||
|
return
|
||||||
|
|
||||||
|
self._server.close()
|
||||||
|
|
||||||
|
|
||||||
|
def start(self, log=True):
|
||||||
|
if self._server:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.cfg.socket:
|
||||||
|
logging.info(f'Starting server on {self.cfg.socket}')
|
||||||
|
|
||||||
|
server = asyncio.start_unix_server(
|
||||||
|
self.handle_client,
|
||||||
|
path = self.cfg.socket
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.info(f'Starting server on {self.cfg.listen}:{self.cfg.port}')
|
||||||
|
|
||||||
|
server = asyncio.start_server(
|
||||||
|
self.handle_client,
|
||||||
|
host = self.cfg.listen,
|
||||||
|
port = self.cfg.port,
|
||||||
|
family = socket.AF_INET,
|
||||||
|
reuse_address = True,
|
||||||
|
reuse_port = True
|
||||||
|
)
|
||||||
|
|
||||||
|
self._server = self.loop.run_until_complete(server)
|
||||||
|
self.loop.run_until_complete(self.handle_run_server())
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_run_server(self):
|
||||||
|
while self._server.is_serving():
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
await self._server.wait_closed()
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
logging.info('Server stopped')
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_client(self, reader, writer):
|
||||||
|
request = None
|
||||||
|
response = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
request = self.cfg.request_class(self, reader, writer.get_extra_info('peername')[0])
|
||||||
|
await request.parse_headers()
|
||||||
|
|
||||||
|
handler = self.get_route(request.path, request.method)
|
||||||
|
|
||||||
|
await self.handle_middleware(request)
|
||||||
|
|
||||||
|
if handler.params:
|
||||||
|
handler_response = await handler.target(request, **handler.params)
|
||||||
|
|
||||||
|
else:
|
||||||
|
handler_response = await handler.target(request)
|
||||||
|
|
||||||
|
if isinstance(handler_response, dict):
|
||||||
|
response = self.cfg.response_class(**handler_response)
|
||||||
|
|
||||||
|
elif isinstance(handler_response, Response):
|
||||||
|
response = handler_response
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise error.ServerError()
|
||||||
|
|
||||||
|
except NotFound:
|
||||||
|
response = self.cfg.response_class.error('Not Found', 404)
|
||||||
|
|
||||||
|
except MethodNotAllowed:
|
||||||
|
response = self.cfg.response_class.error('Method Not Allowed', 405)
|
||||||
|
|
||||||
|
except error.HttpError as e:
|
||||||
|
response = self.cfg.response_class.error(e.message, e.status)
|
||||||
|
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
response = self.cfg.response_class.error('Server Error', 500)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.handle_middleware(request, response)
|
||||||
|
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
response = Response.error('Server Error', 500)
|
||||||
|
|
||||||
|
response.headers.update(self.cfg.default_headers)
|
||||||
|
|
||||||
|
writer.write(response.compile())
|
||||||
|
await writer.drain()
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
if request:
|
||||||
|
logging.info(f'{request.remote} {request.method} {request.path} {response.status} {len(response.body)} {request.agent}')
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_middleware(self, request, response=None):
|
||||||
|
for middleware in self.middleware['response' if response else 'request']:
|
||||||
|
if response:
|
||||||
|
await middleware(request, response)
|
||||||
|
|
||||||
|
else:
|
||||||
|
await middleware(request)
|
85
izzylib/http_server_async/config.py
Normal file
85
izzylib/http_server_async/config.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
from .request import Request
|
||||||
|
from .response import Response
|
||||||
|
|
||||||
|
from .. import __version__
|
||||||
|
from ..config import BaseConfig
|
||||||
|
from ..misc import boolean
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseConfig):
|
||||||
|
_startup = True
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
name = 'IzzyLib Http Server',
|
||||||
|
title = None,
|
||||||
|
version = '0.0.1',
|
||||||
|
git_repo = 'https://git.barkshark.xyz/izaliamae/izzylib',
|
||||||
|
socket = None,
|
||||||
|
listen = 'localhost',
|
||||||
|
host = None,
|
||||||
|
web_host = None,
|
||||||
|
alt_hosts = [],
|
||||||
|
port = 8080,
|
||||||
|
proto = 'http',
|
||||||
|
access_log = True,
|
||||||
|
timeout = 60,
|
||||||
|
default_headers = {},
|
||||||
|
request_class = Request,
|
||||||
|
response_class = Response,
|
||||||
|
sig_handler = None,
|
||||||
|
sig_handler_args = [],
|
||||||
|
sig_handler_kwargs = {},
|
||||||
|
tpl_search = [],
|
||||||
|
tpl_globals = {},
|
||||||
|
tpl_context = None,
|
||||||
|
tpl_autoescape = True,
|
||||||
|
tpl_default = True
|
||||||
|
)
|
||||||
|
|
||||||
|
self._startup = False
|
||||||
|
self.set_data(kwargs)
|
||||||
|
|
||||||
|
self.default_headers['server'] = f'{self.name}/{__version__}'
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(self, key, value):
|
||||||
|
if self._startup:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if key == 'listen':
|
||||||
|
if not self.host:
|
||||||
|
self.host = value
|
||||||
|
|
||||||
|
if not self.web_host:
|
||||||
|
self.web_host = value
|
||||||
|
|
||||||
|
elif key == 'host':
|
||||||
|
if not self.web_host or self.web_host == self.listen:
|
||||||
|
self.web_host = value
|
||||||
|
|
||||||
|
elif key == 'port' and not isinstance(value, int):
|
||||||
|
raise TypeError(f'{key} must be an integer')
|
||||||
|
|
||||||
|
elif key == 'socket':
|
||||||
|
value = Path(value)
|
||||||
|
|
||||||
|
elif key in ['access_log', 'tpl_autoescape', 'tpl_default'] and not isinstance(value, bool):
|
||||||
|
raise TypeError(f'{key} must be a boolean')
|
||||||
|
|
||||||
|
elif key in ['alt_hosts', 'sig_handler_args', 'tpl_search'] and not isinstance(value, list):
|
||||||
|
raise TypeError(f'{key} must be a list')
|
||||||
|
|
||||||
|
elif key in ['sig_handler_kwargs', 'tpl_globals'] and not isinstance(value, dict):
|
||||||
|
raise TypeError(f'{key} must be a dict')
|
||||||
|
|
||||||
|
elif key == 'tpl_context' and not getattr(value, '__call__', None):
|
||||||
|
raise TypeError(f'{key} must be a callable')
|
||||||
|
|
||||||
|
elif key == 'request_class' and not isinstance(value, Request):
|
||||||
|
raise TypeError(f'{key} must be a subclass of izzylib.http_server_async.Request')
|
||||||
|
|
||||||
|
elif key == 'response_class' and not isinstance(value, Response):
|
||||||
|
raise TypeError(f'{key} must be a subclass of izzylib.http_server_async.Response')
|
||||||
|
|
||||||
|
return value
|
36
izzylib/http_server_async/error.py
Normal file
36
izzylib/http_server_async/error.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
class HttpError(Exception):
|
||||||
|
def __init__(self, message, status=500):
|
||||||
|
super().__init__(f'HTTP Error {status}: {message}')
|
||||||
|
|
||||||
|
self.status = status
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(HttpError):
|
||||||
|
def __init__(self, message='Unauthorized'):
|
||||||
|
super().__init__(message, 401)
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(HttpError):
|
||||||
|
def __init__(self, message='Forbidden'):
|
||||||
|
super().__init__(message, 403)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(HttpError):
|
||||||
|
def __init__(self, message='Not Found'):
|
||||||
|
super().__init__(message, 404)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
147
izzylib/http_server_async/request.py
Normal file
147
izzylib/http_server_async/request.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import asyncio, email, traceback
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from urllib.parse import unquote_plus
|
||||||
|
|
||||||
|
from ..dotdict import DotDict, MultiDotDict
|
||||||
|
|
||||||
|
|
||||||
|
UtcTime = timezone.utc
|
||||||
|
LocalTime = datetime.now(UtcTime).astimezone().tzinfo
|
||||||
|
|
||||||
|
|
||||||
|
class Request:
|
||||||
|
__slots__ = ['_body', '_form', '_reader', 'app', 'address', 'method', 'path', 'version', 'headers', 'query', 'raw_query', 'ctx']
|
||||||
|
def __init__(self, app, reader, address):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._reader = reader
|
||||||
|
self._body = b''
|
||||||
|
self._form = DotDict()
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
self.address = address
|
||||||
|
self.method = None
|
||||||
|
self.path = None
|
||||||
|
self.version = None
|
||||||
|
self.headers = MultiDotDict()
|
||||||
|
self.query = DotDict()
|
||||||
|
self.raw_query = None
|
||||||
|
self.ctx = DotDict()
|
||||||
|
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.ctx[key]
|
||||||
|
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.ctx[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
return self.ctx[key]
|
||||||
|
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
if key not in self.__slots__:
|
||||||
|
self.ctx[key] = value
|
||||||
|
|
||||||
|
else:
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def agent(self):
|
||||||
|
return self.headers.get('User-Agent', 'no agent')
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_type(self):
|
||||||
|
return self.headers.get('Content-Type', '')
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
date_str = self.headers.get('Date')
|
||||||
|
|
||||||
|
if date_str:
|
||||||
|
date = datetime.strptime(date_str, '%a, %d %b %Y %H:%M:%S GMT')
|
||||||
|
date = date.replace(tzinfo=UtcTime)
|
||||||
|
return date.astimezone(LocalTime)
|
||||||
|
|
||||||
|
# not sure if this should stay
|
||||||
|
return datetime.now(LocalTime)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
return self.headers.get('Host')
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return int(self.headers.get('Content-Length', 0))
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def remote(self):
|
||||||
|
return self.headers.get('X-Real-Ip', self.headers.get('X-Forwarded-For', self.address))
|
||||||
|
|
||||||
|
|
||||||
|
async def read(self, length=2048, timeout=None):
|
||||||
|
try: return await asyncio.wait_for(self._reader.read(length), timeout or self.app.cfg.timeout)
|
||||||
|
except: return
|
||||||
|
|
||||||
|
|
||||||
|
async def body(self):
|
||||||
|
if not self._body and self.length:
|
||||||
|
self._body = await self.read(self.length)
|
||||||
|
|
||||||
|
return self._body
|
||||||
|
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return (await self.body()).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
async def dict(self):
|
||||||
|
return DotDict(await self.body())
|
||||||
|
|
||||||
|
|
||||||
|
async def form(self):
|
||||||
|
if not self._form and 'application/x-www-form-urlencoded' in self.content_type:
|
||||||
|
for line in unquote_plus(await self.text()).split('&'):
|
||||||
|
try: key, value = line.split('=', 1)
|
||||||
|
except: key, value = line, None
|
||||||
|
|
||||||
|
self._form[key] = value
|
||||||
|
|
||||||
|
return self._form
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_headers(self):
|
||||||
|
data = (await self._reader.readuntil(b'\r\n\r\n')).decode('utf-8')
|
||||||
|
|
||||||
|
for idx, line in enumerate(data.splitlines()):
|
||||||
|
if idx == 0:
|
||||||
|
self.method, path, self.version = line.split()
|
||||||
|
|
||||||
|
try: self.path, self.raw_query = path.split('?', 1)
|
||||||
|
except: self.path = path
|
||||||
|
|
||||||
|
if self.raw_query:
|
||||||
|
for qline in unquote_plus(self.raw_query).split('&'):
|
||||||
|
try: key, value = qline.split('=')
|
||||||
|
except: key, value = qline, None
|
||||||
|
|
||||||
|
self.query[key] = value
|
||||||
|
|
||||||
|
else:
|
||||||
|
try: key, value = line.split(': ')
|
||||||
|
except: continue
|
||||||
|
|
||||||
|
self.headers[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def new_response(self, *args, **kwargs):
|
||||||
|
return self.app.cfg.response_class(*args, **kwargs)
|
86
izzylib/http_server_async/response.py
Normal file
86
izzylib/http_server_async/response.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .cookies import Cookies
|
||||||
|
|
||||||
|
from ..dotdict import MultiDotDict
|
||||||
|
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
__slots__ = ['_body', 'headers', 'cookies', 'status']
|
||||||
|
def __init__(self, body=b'', status=200, headers={}, cookies={}):
|
||||||
|
self._body = b''
|
||||||
|
|
||||||
|
self.body = body
|
||||||
|
self.headers = MultiDotDict(headers)
|
||||||
|
self.cookies = Cookies(cookies)
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
if not self.headers.get('content-type'):
|
||||||
|
self.headers['content-type'] = 'text/plain'
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def body(self):
|
||||||
|
return self._body
|
||||||
|
|
||||||
|
|
||||||
|
@body.setter
|
||||||
|
def body(self, data):
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
self._body += data
|
||||||
|
|
||||||
|
elif isinstance(data, str):
|
||||||
|
self._body += data.encode('utf-8')
|
||||||
|
|
||||||
|
elif any(map(isinstance, [data], [dict, list, tuple])):
|
||||||
|
self._body += json.dumps(data).encode('utf-8')
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._body += str(data).encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def text(cls, *args, **kwargs):
|
||||||
|
return cls(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def html(cls, *args, headers={}, **kwargs):
|
||||||
|
if not headers.get('content-type'):
|
||||||
|
headers['content-type'] = 'text/html'
|
||||||
|
|
||||||
|
return cls(*args, headers=headers, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def json(cls, *args, headers={}, **kwargs):
|
||||||
|
if not headers.get('content-type'):
|
||||||
|
headers['content-type'] = 'application/json'
|
||||||
|
|
||||||
|
return cls(*args, headers=headers, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def error(cls, message, status=500):
|
||||||
|
return cls(f'HTTP Error {status}: {message}', status=status)
|
||||||
|
|
||||||
|
|
||||||
|
def compile(self):
|
||||||
|
data = bytes(f'HTTP/1.1 {self.status}', 'utf-8')
|
||||||
|
|
||||||
|
for k,v in self.headers.items():
|
||||||
|
for value in v:
|
||||||
|
data += bytes(f'\r\n{k.capitalize()}: {value}', 'utf-8')
|
||||||
|
|
||||||
|
if not self.headers.get('content-length'):
|
||||||
|
data += bytes(f'\r\nContent-Length: {len(self.body)}', 'utf-8')
|
||||||
|
|
||||||
|
if not self.headers.get('date'):
|
||||||
|
data += bytes(f'\r\nDate: {datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")}', 'utf-8')
|
||||||
|
|
||||||
|
data += b'\r\n\r\n'
|
||||||
|
data += self.body
|
||||||
|
|
||||||
|
return data
|
30
izzylib/http_server_async/view.py
Normal file
30
izzylib/http_server_async/view.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from . import http_methods
|
||||||
|
|
||||||
|
from ..exceptions import (
|
||||||
|
InvalidMethodException,
|
||||||
|
MethodNotHandledException
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class View:
|
||||||
|
__path__ = ''
|
||||||
|
__slots__ = ['app']
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
|
||||||
|
def get_handler(self, method):
|
||||||
|
if method.upper() not in http_methods:
|
||||||
|
raise InvalidMethodException(method)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return getattr(self, method.lower())
|
||||||
|
except AttributeError:
|
||||||
|
raise MethodNotHandledException(method)
|
||||||
|
|
||||||
|
|
||||||
|
#def get(self, request):
|
||||||
|
#pass
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,6 @@ __all__ = [
|
||||||
'time_function_pprint',
|
'time_function_pprint',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'var_name',
|
'var_name',
|
||||||
'Config',
|
|
||||||
'Url'
|
'Url'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -466,44 +465,6 @@ def var_name(single=True, **kwargs):
|
||||||
return key[0] if single else keys
|
return key[0] if single else keys
|
||||||
|
|
||||||
|
|
||||||
class Config(DotDict):
|
|
||||||
def __init__(self, json_file=None, **defaults):
|
|
||||||
self._defaults = defaults
|
|
||||||
self._json = Path(json_file)
|
|
||||||
|
|
||||||
super().__init__(defaults)
|
|
||||||
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if not key in self._defaults:
|
|
||||||
raise KeyError(f'Not a valid config option: {key}')
|
|
||||||
|
|
||||||
super().__setitem__(key, value)
|
|
||||||
|
|
||||||
|
|
||||||
def reset(self, key=None):
|
|
||||||
if not key:
|
|
||||||
self.update(self._defaults)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self[key] = self._defaults[key]
|
|
||||||
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
try:
|
|
||||||
self.load_json(self._json)
|
|
||||||
return True
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
izzylog.warning('Cannot find path to config file:', self._json)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def save(self, indent='\t'):
|
|
||||||
self._json.parent.mkdir()
|
|
||||||
self.save_json(self._json, indent=indent)
|
|
||||||
|
|
||||||
|
|
||||||
class Url(str):
|
class Url(str):
|
||||||
protocols = {
|
protocols = {
|
||||||
'http': 80,
|
'http': 80,
|
||||||
|
|
143
izzylib/path.py
143
izzylib/path.py
|
@ -5,13 +5,24 @@ from functools import cached_property
|
||||||
from pathlib import Path as PyPath
|
from pathlib import Path as PyPath
|
||||||
|
|
||||||
|
|
||||||
class Path(str):
|
class PathMeta(type):
|
||||||
def __init__(self, path=os.getcwd(), exist=True, missing=True, parents=True):
|
@property
|
||||||
if str(path).startswith('~'):
|
def home(cls):
|
||||||
str.__new__(Path, os.path.expanduser(path))
|
return cls('~').expanduser()
|
||||||
|
|
||||||
else:
|
|
||||||
str.__new__(Path, path)
|
@property
|
||||||
|
def cwd(cls):
|
||||||
|
return cls(os.getcwd()).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
class Path(str, metaclass=PathMeta):
|
||||||
|
def __init__(self, path=os.getcwd(), exist=True, missing=True, parents=True):
|
||||||
|
#if str(path).startswith('~'):
|
||||||
|
#str.__new__(Path, os.path.expanduser(path))
|
||||||
|
|
||||||
|
#else:
|
||||||
|
#str.__new__(Path, path)
|
||||||
|
|
||||||
self.config = {
|
self.config = {
|
||||||
'missing': missing,
|
'missing': missing,
|
||||||
|
@ -32,8 +43,11 @@ class Path(str):
|
||||||
return self.join(key)
|
return self.join(key)
|
||||||
|
|
||||||
|
|
||||||
def __new__(cls, content):
|
def __new__(cls, path):
|
||||||
return str.__new__(cls, content)
|
if str(path).startswith('~'):
|
||||||
|
return str.__new__(cls, os.path.expanduser(path))
|
||||||
|
|
||||||
|
return str.__new__(cls, path)
|
||||||
|
|
||||||
|
|
||||||
def __check_dir(self, path=None):
|
def __check_dir(self, path=None):
|
||||||
|
@ -46,6 +60,51 @@ class Path(str):
|
||||||
raise FileExistsError('File or directory already exists:', target)
|
raise FileExistsError('File or directory already exists:', target)
|
||||||
|
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def isdir(self):
|
||||||
|
return os.path.isdir(self)
|
||||||
|
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def isfile(self):
|
||||||
|
return os.path.isfile(self)
|
||||||
|
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def islink(self):
|
||||||
|
return os.path.islink(self)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mtime(self):
|
||||||
|
return os.path.getmtime(self)
|
||||||
|
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def name(self):
|
||||||
|
return os.path.basename(self)
|
||||||
|
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def parent(self):
|
||||||
|
return Path(os.path.dirname(self))
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
return os.path.getsize(self)
|
||||||
|
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def stem(self):
|
||||||
|
return os.path.basename(self).split('.')[0]
|
||||||
|
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def suffix(self):
|
||||||
|
return os.path.splitext(self)[1]
|
||||||
|
|
||||||
|
|
||||||
def append(self, text):
|
def append(self, text):
|
||||||
return Path(self + text)
|
return Path(self + text)
|
||||||
|
|
||||||
|
@ -88,6 +147,10 @@ class Path(str):
|
||||||
return not self.exists
|
return not self.exists
|
||||||
|
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return os.path.exists(self)
|
||||||
|
|
||||||
|
|
||||||
def expanduser(self):
|
def expanduser(self):
|
||||||
return Path(os.path.expanduser(self))
|
return Path(os.path.expanduser(self))
|
||||||
|
|
||||||
|
@ -161,6 +224,10 @@ class Path(str):
|
||||||
return fd.readlines()
|
return fd.readlines()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(self):
|
||||||
|
return Path(os.path.abspath(self))
|
||||||
|
|
||||||
|
|
||||||
def touch(self, mode=0o644, utime=None):
|
def touch(self, mode=0o644, utime=None):
|
||||||
timestamp = utime or datetime.now().timestamp()
|
timestamp = utime or datetime.now().timestamp()
|
||||||
|
|
||||||
|
@ -174,63 +241,3 @@ class Path(str):
|
||||||
def write(self, data, mode='w'):
|
def write(self, data, mode='w'):
|
||||||
with self.open(mode) as fd:
|
with self.open(mode) as fd:
|
||||||
fd.write(data)
|
fd.write(data)
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exists(self):
|
|
||||||
return os.path.exists(self)
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def home(self):
|
|
||||||
return Path('~')
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def isdir(self):
|
|
||||||
return os.path.isdir(self)
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def isfile(self):
|
|
||||||
return os.path.isfile(self)
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def islink(self):
|
|
||||||
return os.path.islink(self)
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mtime(self):
|
|
||||||
return os.path.getmtime(self)
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def name(self):
|
|
||||||
return os.path.basename(self)
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def parent(self):
|
|
||||||
return Path(os.path.dirname(self))
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def resolve(self):
|
|
||||||
return Path(os.path.abspath(self))
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size(self):
|
|
||||||
return os.path.getsize(self)
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def stem(self):
|
|
||||||
return os.path.basename(self).split('.')[0]
|
|
||||||
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def suffix(self):
|
|
||||||
return os.path.splitext(self)[1]
|
|
||||||
|
|
|
@ -46,6 +46,8 @@ hasher =
|
||||||
http_server =
|
http_server =
|
||||||
sanic == 21.6.2
|
sanic == 21.6.2
|
||||||
envbash == 1.2.0
|
envbash == 1.2.0
|
||||||
|
http_server_async =
|
||||||
|
http_router == 2.6.4
|
||||||
http_signatures =
|
http_signatures =
|
||||||
pycryptodome == 3.10.1
|
pycryptodome == 3.10.1
|
||||||
tldextract == 3.1.2
|
tldextract == 3.1.2
|
||||||
|
|
Loading…
Reference in a new issue