first draft of http_server
This commit is contained in:
parent
62a2ab7115
commit
bf8750a196
9
http_server/izzylib/http_server/__init__.py
Normal file
9
http_server/izzylib/http_server/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from datetime import datetime
|
||||
|
||||
start_time = datetime.now()
|
||||
|
||||
from .application import Application
|
||||
from .config import Config
|
||||
from .request import Request
|
||||
from .response import Response
|
||||
from .view import View
|
106
http_server/izzylib/http_server/application.py
Normal file
106
http_server/izzylib/http_server/application.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
import multiprocessing, sanic, signal, traceback
|
||||
import logging as pylog
|
||||
|
||||
|
||||
from multidict import CIMultiDict
|
||||
from multiprocessing import cpu_count, current_process
|
||||
from urllib.parse import parse_qsl, urlparse
|
||||
|
||||
from izzylib import DotDict, Path, logging
|
||||
from izzylib.template import Template
|
||||
|
||||
from .config import Config
|
||||
from .error_handlers import GenericError, MissingTemplateError
|
||||
from .middleware import AccessLog, Headers
|
||||
from .view import Manifest, Style
|
||||
|
||||
|
||||
log_path_ignore = [
|
||||
'/media',
|
||||
'/static'
|
||||
]
|
||||
|
||||
log_ext_ignore = [
|
||||
'js', 'ttf', 'woff2',
|
||||
'ac3', 'aiff', 'flac', 'm4a', 'mp3', 'ogg', 'wav', 'wma',
|
||||
'apng', 'ico', 'jpeg', 'jpg', 'png', 'svg',
|
||||
'divx', 'mov', 'mp4', 'webm', 'wmv'
|
||||
]
|
||||
|
||||
frontend = Path(__file__).resolve.parent.join('frontend')
|
||||
|
||||
class Application(sanic.Sanic):
|
||||
def __init__(self, **kwargs):
|
||||
self.cfg = Config(**kwargs)
|
||||
|
||||
super().__init__(self.cfg.name, request_class=self.cfg.request_class)
|
||||
|
||||
for log in ['sanic.root', 'sanic.access']:
|
||||
pylog.getLogger(log).setLevel(pylog.WARNING)
|
||||
|
||||
self.template = Template(
|
||||
self.cfg.tpl_search,
|
||||
self.cfg.tpl_globals,
|
||||
self.cfg.tpl_context,
|
||||
self.cfg.tpl_autoescape
|
||||
)
|
||||
|
||||
self.template.add_env('cfg', self.cfg)
|
||||
|
||||
if self.cfg.tpl_default:
|
||||
self.template.add_search_path(frontend)
|
||||
self.add_class_route(Manifest)
|
||||
self.add_class_route(Style)
|
||||
self.static('/favicon.ico', frontend.join('static/icon64.png'))
|
||||
self.static('/framework/static', frontend.join('static'))
|
||||
|
||||
self.add_error_handler(MissingTemplateError)
|
||||
self.add_error_handler(GenericError)
|
||||
|
||||
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 add_class_route(self, cls):
|
||||
for route in cls.paths:
|
||||
self.add_route(cls.as_view(), route)
|
||||
|
||||
|
||||
def add_error_handler(self, handler):
|
||||
handle = handler(self)
|
||||
self.error_handler.add(*handle())
|
||||
|
||||
|
||||
def add_middleware(self, middleware):
|
||||
mw = middleware(self)
|
||||
self.register_middleware(mw.handler, mw.attach)
|
||||
|
||||
|
||||
def start(self):
|
||||
# register built-in middleware now so they're last in the chain
|
||||
self.add_middleware(Headers)
|
||||
self.add_middleware(AccessLog)
|
||||
|
||||
msg = f'Starting {self.cfg.name} at {self.cfg.host}:{self.cfg.port}'
|
||||
|
||||
if self.cfg.workers > 1:
|
||||
msg += f' with {self.cfg.workers} workers'
|
||||
|
||||
logging.info(msg)
|
||||
self.run(
|
||||
host = self.cfg.listen,
|
||||
port = self.cfg.port,
|
||||
workers = self.cfg.workers,
|
||||
access_log = False,
|
||||
debug = False
|
||||
)
|
||||
|
||||
|
||||
def finish(self):
|
||||
if self.cfg.sig_handler:
|
||||
self.cfg.sig_handler(*self.cfg.sig_handler_args, **self.cfg.sig_handler_kwargs)
|
||||
|
||||
self.stop()
|
||||
logging.info('Bye! :3')
|
48
http_server/izzylib/http_server/config.py
Normal file
48
http_server/izzylib/http_server/config.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from izzylib import DotDict
|
||||
from multiprocessing import cpu_count
|
||||
|
||||
from .request import Request
|
||||
from .response import Response
|
||||
|
||||
|
||||
class Config(DotDict):
|
||||
defaults = dict(
|
||||
name = 'IzzyLib Http Server',
|
||||
version = '0.0.1',
|
||||
git_repo = 'https://git.barkshark.xyz/izaliamae/izzylib',
|
||||
listen = 'localhost',
|
||||
host = 'localhost',
|
||||
web_host = 'localhost',
|
||||
alt_hosts = [],
|
||||
port = 8080,
|
||||
proto = 'http',
|
||||
workers = cpu_count(),
|
||||
request_class = Request,
|
||||
response_class = Response,
|
||||
sig_handler = None,
|
||||
sig_handler_args = [],
|
||||
sig_handler_kwargs = {},
|
||||
menu = {},
|
||||
tpl_search = [],
|
||||
tpl_globals = {},
|
||||
tpl_context = None,
|
||||
tpl_autoescape = True,
|
||||
tpl_default = True
|
||||
)
|
||||
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__({**self.defaults, **kwargs})
|
||||
|
||||
if kwargs.get('host') and not kwargs.get('web_host'):
|
||||
self.web_host = self.host
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self.defaults.keys():
|
||||
raise KeyError(f'Invalid config key {key}')
|
||||
|
||||
if key == 'port' and not isinstance(value, int):
|
||||
raise TypeError('Port must be an integer')
|
||||
|
||||
super().__setitem__(key, value)
|
47
http_server/izzylib/http_server/error_handlers.py
Normal file
47
http_server/izzylib/http_server/error_handlers.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
import traceback
|
||||
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
from .response import Response
|
||||
|
||||
|
||||
class GenericError:
|
||||
error = Exception
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
|
||||
def __call__(self):
|
||||
return self.error, self.handler
|
||||
|
||||
|
||||
def handler(self, request, exception):
|
||||
response = Response(self.app, request)
|
||||
|
||||
try:
|
||||
status = exception.status_code
|
||||
msg = str(exception)
|
||||
except:
|
||||
msg = f'{exception.__class__.__name__}: {str(exception)}'
|
||||
status = 500
|
||||
|
||||
if status not in range(200, 499):
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
return response.error(msg, status)
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return response.text(f'{exception.__class__.__name__}: {msg}', status=500)
|
||||
|
||||
|
||||
class MissingTemplateError(GenericError):
|
||||
error = TemplateNotFound
|
||||
|
||||
|
||||
def handler(self, request, exception):
|
||||
logging.error('TEMPLATE_ERROR:', f'{exception.__class__.__name__}: {str(exception)}')
|
||||
return request.response.html('I\'m a dingleberry and forgot to create a template for this page', 500)
|
446
http_server/izzylib/http_server/frontend/base.css
Normal file
446
http_server/izzylib/http_server/frontend/base.css
Normal file
|
@ -0,0 +1,446 @@
|
|||
:root {
|
||||
--text: #eee;
|
||||
--hover: {{primary.desaturate(50).lighten(50)}};
|
||||
--primary: {{primary}};
|
||||
--background: {{background}};
|
||||
--ui: {{primary.desaturate(25).lighten(5)}};
|
||||
--ui-background: {{background.lighten(7.5)}};
|
||||
--shadow-color: {{black.rgba(25)}};
|
||||
--shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color);
|
||||
|
||||
--negative: {{negative}};
|
||||
--negative-dark: {{negative.darken(85)}};
|
||||
--positive: {{positive}};
|
||||
--positive-dark: {{positive.darken(85)}};
|
||||
|
||||
--message: var(--positive);
|
||||
--error: var(--negative);
|
||||
--gap: 15px;
|
||||
--easing: cubic-bezier(.6, .05, .28, .91);
|
||||
--trans-speed: {{speed}}ms;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text);
|
||||
background-color: var(--background);
|
||||
font-family: sans undertale;
|
||||
font-size: 16px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
input:not([type='checkbox']), select, textarea {
|
||||
margin: 1px 0;
|
||||
color: var(--text);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--background);
|
||||
box-shadow: 0 2px 2px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
input:hover, select:hover, textarea:hover {
|
||||
border-color: var(--hover);
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: 0;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
details:focus, summary:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Classes */
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
background-color: {{primary.darken(85)}};
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: {{primary.darken(65)}};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-gap: var(--gap);
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
display: inline-grid;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-wrap; wrap;
|
||||
}
|
||||
|
||||
.menu {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu li {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
background-color: {{background.lighten(20)}};
|
||||
}
|
||||
|
||||
.menu li a {
|
||||
display: block;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.menu li:hover {
|
||||
background-color: {{primary.lighten(25).desaturate(25)}};
|
||||
}
|
||||
|
||||
.menu li a:hover {
|
||||
text-decoration: none;
|
||||
color: {{primary.darken(90).desaturate(50)}};
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 8px;
|
||||
background-color: var(--ui-background);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
.message {
|
||||
line-height: 2em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* # this is kinda hacky and needs to be replaced */
|
||||
.tooltip:hover::after {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
bottom: 35px;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--text);
|
||||
color: var(--text);
|
||||
background-color: {{primary.desaturate(50).darken(75)}};
|
||||
box-shadow: var(--shadow);
|
||||
/*z-index: -1;*/
|
||||
}
|
||||
|
||||
|
||||
/* ids */
|
||||
#title {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#message, #error {
|
||||
padding: 10px;
|
||||
color: var(--background);
|
||||
margin-bottom: var(--gap);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#message {
|
||||
background-color: var(--message);
|
||||
}
|
||||
|
||||
#error {
|
||||
background-color: var(--error);
|
||||
}
|
||||
|
||||
#body {
|
||||
width: 790px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#header {
|
||||
display: flex;
|
||||
margin-bottom: var(--gap);
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
line-height: 40px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#header > div {
|
||||
/*display: inline-block;*/
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
#header .page-title {
|
||||
text-align: {% if menu_left %}right{% else %}left{% endif %};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content-body .title {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: var(--primary)
|
||||
}
|
||||
|
||||
#footer {
|
||||
margin-top: var(--gap);
|
||||
display: flex;
|
||||
grid-gap: 5px;
|
||||
font-size: 0.80em;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#footer > div {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#footer .avatar img {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#footer .user {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#footer .source {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
{% for name in cssfiles %}
|
||||
{% include 'style/' + name + '.css' %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
/* responsive design */
|
||||
@media (max-width: 810px) {
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#body {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 610px) {
|
||||
.settings .grid-container {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
.settings .label {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Main menu */
|
||||
#btn {
|
||||
cursor: pointer;
|
||||
transition: left 500ms var(--easing);
|
||||
}
|
||||
|
||||
#btn {
|
||||
transition: background-color var(--trans-speed);
|
||||
width: 55px;
|
||||
margin-left: var(--gap);
|
||||
background-image: url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/menu.svg');
|
||||
background-size: 50px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#btn div {
|
||||
transition: transform var(--trans-speed) ease, opacity var(--trans-speed), background-color var(--trans-speed);
|
||||
}
|
||||
|
||||
#btn.active {
|
||||
margin-left: 0;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
top: 12px;
|
||||
{% if menu_left %}right: calc(100% - 250px + 12px){% else %}right: 12px;{% endif %};
|
||||
background-color: {{primary.darken(75)}};
|
||||
color: {{background}};
|
||||
}
|
||||
|
||||
/*#btn.active div {
|
||||
width: 35px;
|
||||
height: 2px;
|
||||
margin-bottom: 8px;
|
||||
}*/
|
||||
|
||||
#btn.active:parent {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
|
||||
#menu {
|
||||
position: fixed;
|
||||
z-index: 4;
|
||||
overflow: auto;
|
||||
top: 0px;
|
||||
opacity: 0;
|
||||
padding: 20px 0px;
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
transition: all var(--trans-speed) ease;
|
||||
{% if menu_left %}left{% else %}right{% endif %}: -250px;
|
||||
}
|
||||
|
||||
#menu.active {
|
||||
{% if menu_left %}left{% else %}right{% endif %}: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#menu #items {
|
||||
/*margin-top: 50px;*/
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
#menu a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#menu {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#menu .item {
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 2em;
|
||||
transition: all var(--trans-speed);
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#menu .title-item {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
#items .sub-item {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
#items .item:not(.title-item):hover {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
#items .sub-item:hover {
|
||||
padding-left: 60px !important;
|
||||
}
|
||||
|
||||
/*#menu details .item:hover {
|
||||
padding-left: 60px;
|
||||
}*/
|
||||
|
||||
#items summary {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
#items details[open]>.item:not(details) {
|
||||
animation-name: fadeInDown;
|
||||
animation-duration: var(--trans-speed);
|
||||
}
|
||||
|
||||
|
||||
#items summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#items details summary:after {
|
||||
content: " +";
|
||||
}
|
||||
|
||||
#items details[open] summary:after {
|
||||
content: " -";
|
||||
}
|
||||
|
||||
|
||||
#btn, #btn * {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#menu {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeInDown {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-1.25em);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* scrollbar */
|
||||
body {scrollbar-width: 15px; scrollbar-color: var(--primary) {{background.darken(10)}};}
|
||||
::-webkit-scrollbar {width: 15px;}
|
||||
::-webkit-scrollbar-track {background: {{background.darken(10)}};}
|
||||
/*::-webkit-scrollbar-button {background: var(--primary);}
|
||||
::-webkit-scrollbar-button:hover {background: var(--text);}*/
|
||||
::-webkit-scrollbar-thumb {background: var(--primary);}
|
||||
::-webkit-scrollbar-thumb:hover {background: {{primary.lighten(25)}};}
|
||||
|
||||
|
||||
/* page font */
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Bold'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-SemiBold.woff2') format('woff2'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-SemiBold.ttf') format('ttf');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Light Italic'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-ExtraLightItalic.woff2') format('woff2'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-ExtraLightItalic.ttf') format('ttf');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Bold Italic'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Italic.woff2') format('woff2'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Italic.ttf') format('ttf');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Light'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Light.woff2') format('woff2'),
|
||||
url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Light.ttf') format('ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
42
http_server/izzylib/http_server/frontend/base.haml
Normal file
42
http_server/izzylib/http_server/frontend/base.haml
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
%html
|
||||
%head
|
||||
%title << {{cfg.name}}: {{page}}
|
||||
%link rel='stylesheet' type='text/css' href='{{cfg.proto}}://{{cfg.web_host}}/framework/style.css'
|
||||
%link rel='manifest' href='{{cfg.proto}}://{{cfg.web_host}}/framework/manifest.json'
|
||||
%meta charset='UTF-8'
|
||||
%meta name='viewport' content='width=device-width, initial-scale=1'
|
||||
|
||||
%body
|
||||
#body
|
||||
#header.flex-container
|
||||
-if menu_left
|
||||
#btn.section
|
||||
.page-title.section -> %a.title href='{{cfg.proto}}://{{cfg.web_host}}/' << {{cfg.name}}
|
||||
|
||||
-else
|
||||
.page-title.section -> %a.title href='{{cfg.proto}}://{{cfg.web_host}}/' << {{cfg.name}}
|
||||
#btn.section
|
||||
|
||||
-if message
|
||||
#message.section << {{message}}
|
||||
|
||||
-if error
|
||||
#error.secion << {{error}}
|
||||
|
||||
#menu.section
|
||||
.title-item.item << Menu
|
||||
#items
|
||||
-for label, path in cfg.menu.items()
|
||||
.item -> %a href='{{cfg.proto}}://{{cfg.web_host}}{{path}}' << {{label}}
|
||||
|
||||
#content-body.section
|
||||
-block content
|
||||
|
||||
#footer.grid-container.section
|
||||
.avatar
|
||||
.user
|
||||
.source
|
||||
%a href='{{cfg.git_repo}}' target='_new' << {{cfg.name}}/{{cfg.version}}
|
||||
|
||||
%script type='application/javascript' src='{{cfg.proto}}://{{cfg.web_host}}/framework/static/menu.js'
|
8
http_server/izzylib/http_server/frontend/error.haml
Normal file
8
http_server/izzylib/http_server/frontend/error.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Error'
|
||||
-block content
|
||||
%center
|
||||
%font size='8'
|
||||
HTTP {{response.status}}
|
||||
%br
|
||||
=error_message
|
BIN
http_server/izzylib/http_server/frontend/static/icon512.png
Normal file
BIN
http_server/izzylib/http_server/frontend/static/icon512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
BIN
http_server/izzylib/http_server/frontend/static/icon64.png
Normal file
BIN
http_server/izzylib/http_server/frontend/static/icon64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
29
http_server/izzylib/http_server/frontend/static/menu.js
Normal file
29
http_server/izzylib/http_server/frontend/static/menu.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const sidebarBox = document.querySelector('#menu'),
|
||||
sidebarBtn = document.querySelector('#btn'),
|
||||
pageWrapper = document.querySelector('html');
|
||||
header = document.querySelector('#header')
|
||||
|
||||
sidebarBtn.addEventListener('click', event => {
|
||||
sidebarBtn.classList.toggle('active');
|
||||
sidebarBox.classList.toggle('active');
|
||||
});
|
||||
|
||||
pageWrapper.addEventListener('click', event => {
|
||||
itemId = event.srcElement.id
|
||||
itemClass = event.srcElement.className
|
||||
indexId = ['menu', 'btn', 'items'].indexOf(itemId)
|
||||
indexClass = ['item', 'item name', 'items'].indexOf(itemClass)
|
||||
|
||||
if (sidebarBox.classList.contains('active') && (indexId == -1 && indexClass == -1)) {
|
||||
sidebarBtn.classList.remove('active');
|
||||
sidebarBox.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', event => {
|
||||
|
||||
if (sidebarBox.classList.contains('active') && event.keyCode === 27) {
|
||||
sidebarBtn.classList.remove('active');
|
||||
sidebarBox.classList.remove('active');
|
||||
}
|
||||
});
|
84
http_server/izzylib/http_server/frontend/static/menu.svg
Normal file
84
http_server/izzylib/http_server/frontend/static/menu.svg
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
sodipodi:docname="menu.svg"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 132.29167 79.375002"
|
||||
height="300"
|
||||
width="500">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-x="36"
|
||||
inkscape:window-height="990"
|
||||
inkscape:window-width="1644"
|
||||
units="px"
|
||||
showgrid="true"
|
||||
inkscape:document-rotation="0"
|
||||
inkscape:current-layer="layer2"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:cy="151.34478"
|
||||
inkscape:cx="232.18877"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base"
|
||||
inkscape:snap-text-baseline="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0">
|
||||
<inkscape:grid
|
||||
dotted="true"
|
||||
id="grid1402"
|
||||
type="xygrid"
|
||||
originx="-7.9375001"
|
||||
originy="-27.781234" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
id="layer2"
|
||||
inkscape:groupmode="layer"
|
||||
transform="translate(-7.9374999,-27.781233)">
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 15.875,67.468765 c 116.41667,0 116.41667,0 116.41667,0 z"
|
||||
id="path1590" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 15.875,35.718766 c 116.41667,0 116.41667,0 116.41667,0 z"
|
||||
id="path1590-7" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 15.875,99.218766 c 116.41667,0 116.41667,0 116.41667,0 z"
|
||||
id="path1590-7-8" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,44 @@
|
|||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
40
http_server/izzylib/http_server/middleware.py
Normal file
40
http_server/izzylib/http_server/middleware.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
import multiprocessing
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from izzylib import logging
|
||||
|
||||
from . import start_time
|
||||
|
||||
|
||||
class MiddlewareBase:
|
||||
attach = 'request'
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.cfg = app.cfg
|
||||
|
||||
|
||||
async def handler(self, request):
|
||||
pass
|
||||
|
||||
|
||||
class Headers(MiddlewareBase):
|
||||
attach = 'response'
|
||||
|
||||
async def handler(self, request, response):
|
||||
if request.path.startswith('/framework') or request.path == '/favicon.ico':
|
||||
max_age = int(timedelta(weeks=2).total_seconds())
|
||||
response.headers['Cache-Control'] = f'immutable,private,max-age={max_age}'
|
||||
|
||||
response.headers['Server'] = f'{self.cfg.name}/{self.cfg.version}'
|
||||
response.headers['Trans'] = 'Rights'
|
||||
|
||||
|
||||
class AccessLog(MiddlewareBase):
|
||||
attach = 'response'
|
||||
|
||||
async def handler(self, request, response):
|
||||
uagent = request.headers.get('user-agent', 'None')
|
||||
address = request.headers.get('x-real-ip', request.forwarded.get('for', request.remote_addr))
|
||||
|
||||
logging.info(f'({multiprocessing.current_process().name}) {address} {request.method} {request.path} {response.status} "{uagent}"')
|
31
http_server/izzylib/http_server/misc.py
Normal file
31
http_server/izzylib/http_server/misc.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from izzylib import LowerDotDict
|
||||
|
||||
|
||||
def ReplaceHeader(headers, key, value):
|
||||
for k,v in headers.items():
|
||||
if k.lower() == header.lower():
|
||||
del headers[k]
|
||||
|
||||
|
||||
class Headers(LowerDotDict):
|
||||
def __init__(self, headers):
|
||||
super().__init__()
|
||||
|
||||
for k,v in headers.items():
|
||||
if not self.get(k):
|
||||
self[k] = []
|
||||
|
||||
self[k].append(v)
|
||||
|
||||
|
||||
def getone(self, key, default=None):
|
||||
value = self.get(key)
|
||||
|
||||
if not value:
|
||||
return default
|
||||
|
||||
return value[0]
|
||||
|
||||
|
||||
def getall(self, key, default=[]):
|
||||
return self.get(key.lower(), default)
|
103
http_server/izzylib/http_server/request.py
Normal file
103
http_server/izzylib/http_server/request.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
import sanic
|
||||
|
||||
from .misc import Headers
|
||||
|
||||
|
||||
class Request(sanic.request.Request):
|
||||
def __init__(self, url_bytes, headers, version, method, transport, app):
|
||||
super().__init__(url_bytes, headers, version, method, transport, app)
|
||||
|
||||
self.Headers = Headers(headers)
|
||||
self.Data = Data(self)
|
||||
self.template = self.app.template
|
||||
self.setup()
|
||||
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
|
||||
def response(self, tpl, *args, **kwargs):
|
||||
return self.template.response(self, tpl, *args, **kwargs)
|
||||
|
||||
|
||||
def alldata(self):
|
||||
return DotDict(
|
||||
**self.content.json,
|
||||
**self.data.query,
|
||||
**self.data.form
|
||||
)
|
||||
|
||||
|
||||
def json_check(self):
|
||||
if self.path.endswith('.json'):
|
||||
return True
|
||||
|
||||
accept = self.headers.getone('Accept', None)
|
||||
|
||||
if accept:
|
||||
mimes = [v.strip() for v in accept.split(',')]
|
||||
|
||||
if any(mime in ['application/json', 'application/activity+json'] for mime in mimes):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Data(object):
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
|
||||
@property
|
||||
def combined(self):
|
||||
return DotDict(**self.form.asDict(), **self.query.asDict(), **self.json.asDict())
|
||||
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
data = {k: v for k,v in parse_qsl(self.request.query_string)}
|
||||
return DotDict(data)
|
||||
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
data = {k: v[0] for k,v in self.request.form.items()}
|
||||
return DotDict(data)
|
||||
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
return DotDict({k:v[0] for k,v in self.request.files.items()})
|
||||
|
||||
|
||||
### body functions
|
||||
@property
|
||||
def raw(self):
|
||||
try:
|
||||
return self.request.body
|
||||
except Exception as e:
|
||||
logging.verbose('IzzyLib.http_server.Data.raw: failed to get body')
|
||||
logging.debug(f'{e.__class__.__name__}: {e}')
|
||||
return b''
|
||||
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
try:
|
||||
return self.raw.decode()
|
||||
except Exception as e:
|
||||
logging.verbose('IzzyLib.http_server.Data.text: failed to get body')
|
||||
logging.debug(f'{e.__class__.__name__}: {e}')
|
||||
return ''
|
||||
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
try:
|
||||
return DotDict(self.text)
|
||||
except Exception as e:
|
||||
logging.verbose('IzzyLib.http_server.Data.json: failed to get body')
|
||||
logging.debug(f'{e.__class__.__name__}: {e}')
|
||||
data = '{}'
|
||||
return {}
|
211
http_server/izzylib/http_server/response.py
Normal file
211
http_server/izzylib/http_server/response.py
Normal file
|
@ -0,0 +1,211 @@
|
|||
import json, sanic
|
||||
|
||||
from izzylib import DotDict
|
||||
from izzylib.template import Color
|
||||
from sanic.compat import Header
|
||||
from sanic.cookies import CookieJar
|
||||
|
||||
|
||||
class Response:
|
||||
content_types = DotDict({
|
||||
'text': 'text/plain',
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'javascript': 'application/javascript',
|
||||
'json': 'application/json',
|
||||
'activitypub': 'application/activity+json'
|
||||
})
|
||||
|
||||
default_theme = DotDict({
|
||||
'primary': Color('#e7a'),
|
||||
'secondary': Color('#a7e'),
|
||||
'background': Color('#191919'),
|
||||
'positive': Color('#aea'),
|
||||
'negative': Color('#e99'),
|
||||
'white': Color('#eee'),
|
||||
'black': Color('#111'),
|
||||
'speed': 250
|
||||
})
|
||||
|
||||
|
||||
def __init__(self, app, request, body=None, headers={}, cookies={}, status=200, content_type='text/html'):
|
||||
# server objects
|
||||
self.app = app
|
||||
self.cfg = app.cfg
|
||||
self.request = request
|
||||
|
||||
# init vars
|
||||
self._body = None
|
||||
self._content_type = content_type
|
||||
|
||||
self.headers = Header(headers)
|
||||
self.cookies = CookieJar(self.headers)
|
||||
self.body = body
|
||||
self.status = status
|
||||
|
||||
for cookie in cookies.items():
|
||||
pass
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.body
|
||||
|
||||
|
||||
def __bytes__(self):
|
||||
return self.body.encode('utf-8')
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.get_response()
|
||||
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
|
||||
@body.setter
|
||||
def body(self, body):
|
||||
if not body:
|
||||
self._body = b''
|
||||
return
|
||||
|
||||
if self.content_type in [self.content_types.json, self.content_types.activitypub]:
|
||||
body = json.dumps(body)
|
||||
|
||||
if isinstance(body, str):
|
||||
self._body = body.encode('utf-8')
|
||||
|
||||
elif isinstance(body, bytes):
|
||||
self._body = body
|
||||
|
||||
else:
|
||||
raise TypeError(f'Response body must be a string or bytes, not {body.__class__.__name__}')
|
||||
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return self._content_type
|
||||
|
||||
|
||||
@content_type.setter
|
||||
def content_type(self, ctype):
|
||||
self._content_type = self.content_types.get(ctype, ctype)
|
||||
|
||||
|
||||
def set_headers(self, data: dict):
|
||||
try:
|
||||
self.set_content_type(headers.pop('content-type'))
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
self.headers.clear()
|
||||
self.headers.update(data)
|
||||
|
||||
|
||||
def set_cookie(self, key, value, data={}, **kwargs):
|
||||
self.cookies[key] = value
|
||||
|
||||
data.update(kwargs)
|
||||
|
||||
for k,v in data.items():
|
||||
if k.lower() == 'max-age':
|
||||
if isinstance(v, timedelta):
|
||||
v = int(v.total_seconds())
|
||||
|
||||
elif not isinstance(v, int):
|
||||
raise TypeError('Max-Age must be an integer or timedelta')
|
||||
|
||||
elif k.lower() == 'expires':
|
||||
if isinstance(v, datetime):
|
||||
v = v.strftime('%a, %d-%b-%Y %T GMT')
|
||||
|
||||
elif not isinstance(v, str):
|
||||
raise TypeError('Expires must be a string or datetime')
|
||||
|
||||
self.cookies[key][k] = v
|
||||
|
||||
|
||||
def get_cookie(self, key):
|
||||
try:
|
||||
cookie = self.cookies[key]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
return
|
||||
|
||||
|
||||
def del_cookie(self, key):
|
||||
del self.cookies[key]
|
||||
|
||||
|
||||
def template(self, tplfile, context={}, headers={}, status=200, content_type='text/html', cookies={}, pprint=False):
|
||||
self.status = status
|
||||
context.update({
|
||||
'response': self,
|
||||
**self.default_theme
|
||||
})
|
||||
|
||||
html = self.app.template.render(tplfile, context, request=self, pprint=pprint)
|
||||
return self.html(html, headers=headers, status=status, content_type=content_type, cookies=cookies)
|
||||
|
||||
|
||||
def error(self, message, status=500, **kwargs):
|
||||
if self.request and 'json' in self.request.headers.get('accept', ''):
|
||||
return self.json({f'error {status}': message}, status=status, **kwargs)
|
||||
|
||||
return self.template('error.haml', {'error_message': message}, status=status, **kwargs)
|
||||
|
||||
|
||||
def json(self, body={}, headers={}, status=200, content_type='application/json', cookies={}):
|
||||
body = json.dumps(body)
|
||||
return self.get_response(body, headers, status, content_type, cookies)
|
||||
|
||||
|
||||
def text(self, body, headers={}, status=200, content_type='text/plain', cookies={}):
|
||||
return self.get_response(body, headers, status, content_type, cookies)
|
||||
|
||||
|
||||
def html(self, *args, **kwargs):
|
||||
self.content_type = 'text/html'
|
||||
return self.text(*args, **kwargs)
|
||||
|
||||
|
||||
def css(self, *args, **kwargs):
|
||||
self.content_type = 'text/css'
|
||||
return self.text.text(*args, **kwargs)
|
||||
|
||||
|
||||
def javascript(self, *args, **kwargs):
|
||||
self.content_type = 'application/javascript'
|
||||
return self.text.text(*args, **kwargs)
|
||||
|
||||
|
||||
def activitypub(self, *args, **kwargs):
|
||||
self.content_type = 'application/activity+json'
|
||||
return self.text.text(*args, **kwargs)
|
||||
|
||||
|
||||
def redir(self, path, status=302, headers={}):
|
||||
return sanic.response.redirect(path, status=status, headers={})
|
||||
|
||||
|
||||
def set_data(self, body=None, headers={}, status=200, content_type='text/html', cookies={}):
|
||||
ctype = self.content_types.get(content_type, content_type)
|
||||
|
||||
self.body = body
|
||||
self.headers = headers
|
||||
self.status = status
|
||||
self.content_type = content_type
|
||||
self.headers = headers
|
||||
self.headers.pop('content-type', None)
|
||||
|
||||
|
||||
def get_response(self, *args, **kwargs):
|
||||
self.set_data(*args, **kwargs)
|
||||
|
||||
response = sanic.response.HTTPResponse(self.body, self.status, self.headers, self.content_type)
|
||||
response._cookies = self.cookies
|
||||
|
||||
return response
|
55
http_server/izzylib/http_server/view.py
Normal file
55
http_server/izzylib/http_server/view.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
from izzylib.template import Color
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
from .response import Response
|
||||
|
||||
|
||||
class View(HTTPMethodView):
|
||||
routes = []
|
||||
|
||||
|
||||
def dispatch_request(self, request, *args, **kwargs):
|
||||
self.app = request.app
|
||||
self.cfg = request.app.cfg
|
||||
|
||||
handler = getattr(self, request.method.lower(), None)
|
||||
return handler(request, Response(self.app, request), *args, **kwargs)
|
||||
|
||||
|
||||
class Manifest(View):
|
||||
paths = ['/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}'
|
||||
}
|
||||
|
||||
return response.json(data)
|
||||
|
||||
|
||||
class Style(View):
|
||||
paths = ['/framework/style.css']
|
||||
|
||||
async def get(self, request, response):
|
||||
return response.template('base.css', content_type='text/css')
|
||||
|
43
http_server/setup.py
Normal file
43
http_server/setup.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
from setuptools import setup, find_namespace_packages
|
||||
|
||||
|
||||
requires = [
|
||||
'sanic==20.12.3',
|
||||
'sanic-cors==1.0.0',
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name="IzzyLib HTTP Server",
|
||||
version='0.6.0',
|
||||
packages=find_namespace_packages(include=['izzylib.http_requests_client']),
|
||||
python_requires='>=3.7.0',
|
||||
install_requires=requires,
|
||||
include_package_data=False,
|
||||
author='Zoey Mae',
|
||||
author_email='admin@barkshark.xyz',
|
||||
description='An HTTP server based on Sanic',
|
||||
keywords='web http server',
|
||||
url='https://git.barkshark.xyz/izaliamae/izzylib',
|
||||
project_urls={
|
||||
'Bug Tracker': 'https://git.barkshark.xyz/izaliamae/izzylib/issues',
|
||||
'Documentation': 'https://git.barkshark.xyz/izaliamae/izzylib/wiki',
|
||||
'Source Code': 'https://git.barkshark.xyz/izaliamae/izzylib'
|
||||
},
|
||||
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Information Technology',
|
||||
'License :: Co-operative Non-violent Public License (CNPL 6+)',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules'
|
||||
]
|
||||
)
|
|
@ -107,13 +107,13 @@ class Template(Environment):
|
|||
|
||||
|
||||
def update_filter(self, data):
|
||||
if not isinstance(context, dict):
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f'Filter data not a dict')
|
||||
|
||||
self.filters.update(data)
|
||||
|
||||
|
||||
def render(self, tplfile, context={}, headers={}, cookies={}, request=None, pprint=False, **kwargs):
|
||||
def render(self, tplfile, context={}, headers={}, cookies={}, request=None, pprint=False):
|
||||
if not isinstance(context, dict):
|
||||
raise TypeError(f'context for {tplfile} not a dict: {type(context)} {context}')
|
||||
|
||||
|
|
Loading…
Reference in a new issue