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 .misc import *
|
||||
from .cache import CacheDecorator, LruCache, TtlCache
|
||||
from .config import BaseConfig, JsonConfig, YamlConfig
|
||||
from .connection import Connection
|
||||
|
||||
from .http_client import HttpClient, HttpResponse
|
||||
|
||||
Config = JsonConfig
|
||||
|
||||
|
||||
def log_import_error(package, *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:
|
||||
traceback.print_exc()
|
||||
|
|
|
@ -118,21 +118,24 @@ class LowerDotDict(DotDict):
|
|||
return super().__setitem__(key.lower(), value)
|
||||
|
||||
|
||||
class MultiDotDict(DotDict):
|
||||
def __getattr__(self, key):
|
||||
return self.__getitem__(key)
|
||||
class MultiDotDict(LowerDotDict):
|
||||
def __setitem__(self, key, value, single=False):
|
||||
key = key.lower()
|
||||
|
||||
if single:
|
||||
super().__setitem__(key, [value])
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
try:
|
||||
self.__getitem__(key.lower(), False).append(value)
|
||||
else:
|
||||
try:
|
||||
self.__getitem__(key, False).append(value)
|
||||
|
||||
except KeyError as e:
|
||||
super().__setitem__(key.lower(), [value])
|
||||
except KeyError as e:
|
||||
super().__setitem__(key, [value])
|
||||
|
||||
|
||||
def __getitem__(self, key, single=True):
|
||||
values = super().__getitem__(key.lower())
|
||||
key = key.lower()
|
||||
values = super().__getitem__(key)
|
||||
|
||||
if single:
|
||||
try:
|
||||
|
@ -169,10 +172,7 @@ class MultiDotDict(DotDict):
|
|||
|
||||
|
||||
def set(self, key, value):
|
||||
if self.get(key):
|
||||
del self[key]
|
||||
|
||||
self[key] = value
|
||||
self.__setitem__(key, value, True)
|
||||
|
||||
|
||||
def delone(self, key, value):
|
||||
|
@ -180,7 +180,7 @@ class MultiDotDict(DotDict):
|
|||
|
||||
|
||||
def delete(self, key):
|
||||
self.pop(key)
|
||||
del self[key]
|
||||
|
||||
|
||||
class JsonEncoder(json.JSONEncoder):
|
||||
|
|
|
@ -8,3 +8,15 @@ class DBusServerError(Exception):
|
|||
|
||||
class HttpFileDownloadedError(Exception):
|
||||
'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'
|
||||
]
|
||||
|
||||
frontend = Path(__file__).resolve.parent.join('frontend')
|
||||
frontend = Path(__file__).resolve().parent.join('frontend')
|
||||
|
||||
class Application(sanic.Sanic):
|
||||
_extra = DotDict()
|
||||
|
@ -68,8 +68,9 @@ class Application(sanic.Sanic):
|
|||
self.add_error_handler(MissingTemplateError)
|
||||
self.add_error_handler(GenericError)
|
||||
|
||||
## compat
|
||||
self.start = self.run
|
||||
for sig in signal.valid_signals():
|
||||
if type(sig) != int:
|
||||
self.set_signal(sig)
|
||||
|
||||
|
||||
def __getattr__(self, key):
|
||||
|
@ -138,12 +139,7 @@ class Application(sanic.Sanic):
|
|||
return handler
|
||||
|
||||
|
||||
def run(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)
|
||||
|
||||
def start(self, log=False, async_server=False):
|
||||
# register built-in middleware now so they're last in the chain
|
||||
self.add_middleware(Headers)
|
||||
|
||||
|
@ -167,7 +163,7 @@ class Application(sanic.Sanic):
|
|||
return_asyncio_server = True
|
||||
)
|
||||
|
||||
super().run(
|
||||
self.run(
|
||||
host = self.cfg.listen,
|
||||
port = self.cfg.port,
|
||||
workers = self.cfg.workers,
|
||||
|
@ -181,15 +177,41 @@ class Application(sanic.Sanic):
|
|||
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:
|
||||
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()
|
||||
|
||||
try:
|
||||
self.loop.stop()
|
||||
self.loop.close()
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exception(e)
|
||||
|
||||
#izzylog.info('Bye! :3')
|
||||
#sys.exit()
|
||||
|
||||
|
||||
#async def dispatch(self, *args, **kwargs):
|
||||
#return
|
||||
|
||||
|
||||
def parse_level(level):
|
||||
if type(level) == int:
|
||||
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',
|
||||
'timestamp',
|
||||
'var_name',
|
||||
'Config',
|
||||
'Url'
|
||||
]
|
||||
|
||||
|
@ -466,44 +465,6 @@ def var_name(single=True, **kwargs):
|
|||
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):
|
||||
protocols = {
|
||||
'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
|
||||
|
||||
|
||||
class Path(str):
|
||||
def __init__(self, path=os.getcwd(), exist=True, missing=True, parents=True):
|
||||
if str(path).startswith('~'):
|
||||
str.__new__(Path, os.path.expanduser(path))
|
||||
class PathMeta(type):
|
||||
@property
|
||||
def home(cls):
|
||||
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 = {
|
||||
'missing': missing,
|
||||
|
@ -32,8 +43,11 @@ class Path(str):
|
|||
return self.join(key)
|
||||
|
||||
|
||||
def __new__(cls, content):
|
||||
return str.__new__(cls, content)
|
||||
def __new__(cls, path):
|
||||
if str(path).startswith('~'):
|
||||
return str.__new__(cls, os.path.expanduser(path))
|
||||
|
||||
return str.__new__(cls, path)
|
||||
|
||||
|
||||
def __check_dir(self, path=None):
|
||||
|
@ -46,6 +60,51 @@ class Path(str):
|
|||
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):
|
||||
return Path(self + text)
|
||||
|
||||
|
@ -88,6 +147,10 @@ class Path(str):
|
|||
return not self.exists
|
||||
|
||||
|
||||
def exists(self):
|
||||
return os.path.exists(self)
|
||||
|
||||
|
||||
def expanduser(self):
|
||||
return Path(os.path.expanduser(self))
|
||||
|
||||
|
@ -161,6 +224,10 @@ class Path(str):
|
|||
return fd.readlines()
|
||||
|
||||
|
||||
def resolve(self):
|
||||
return Path(os.path.abspath(self))
|
||||
|
||||
|
||||
def touch(self, mode=0o644, utime=None):
|
||||
timestamp = utime or datetime.now().timestamp()
|
||||
|
||||
|
@ -174,63 +241,3 @@ class Path(str):
|
|||
def write(self, data, mode='w'):
|
||||
with self.open(mode) as fd:
|
||||
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]
|
||||
|
|
Loading…
Reference in a new issue