From 28446d95471e74de364b53c3f24d6182fddac0e0 Mon Sep 17 00:00:00 2001 From: "bozo.kopic" Date: Wed, 16 Aug 2017 15:11:21 +0200 Subject: backend --- node_modules.patch | 21 ++++ requirements.pip.txt | 2 +- schemas_json/server.yaml | 3 +- src_js/hatter/lenses.js | 21 ---- src_js/hatter/main.js | 1 - src_js/hatter/renderer.js | 279 +++++++++++++++++++++++++--------------------- src_js/hatter/util.js | 172 ++++++++++++++++++++++++++++ src_py/hatter/backend.py | 9 ++ src_py/hatter/main.py | 96 ++++++++++++++++ src_py/hatter/server.py | 46 ++++++++ webpack.config.js | 20 +--- 11 files changed, 504 insertions(+), 166 deletions(-) delete mode 100644 src_js/hatter/lenses.js create mode 100644 src_js/hatter/util.js create mode 100644 src_py/hatter/backend.py create mode 100644 src_py/hatter/main.py create mode 100644 src_py/hatter/server.py diff --git a/node_modules.patch b/node_modules.patch index 6ed69bd..716a9c9 100644 --- a/node_modules.patch +++ b/node_modules.patch @@ -11,3 +11,24 @@ } else if (typeof previousValue === "string") { node[propName] = "" } else { + + +--- node_modules/virtual-dom/virtual-hyperscript/index.js ++++ node_modules/virtual-dom/virtual-hyperscript/index.js +@@ -40,16 +40,6 @@ + props.namespace = undefined; + } + +- // fix cursor bug +- if (tag === 'INPUT' && +- !namespace && +- props.hasOwnProperty('value') && +- props.value !== undefined && +- !isHook(props.value) +- ) { +- props.value = softSetHook(props.value); +- } +- + transformProperties(props); + + if (children !== undefined && children !== null) { diff --git a/requirements.pip.txt b/requirements.pip.txt index 4c45f37..cc1b393 100644 --- a/requirements.pip.txt +++ b/requirements.pip.txt @@ -3,4 +3,4 @@ aiohttp libvirt-python pyyaml paramiko -GitPython +setuptools diff --git a/schemas_json/server.yaml b/schemas_json/server.yaml index 89c74e4..da50e06 100644 --- a/schemas_json/server.yaml +++ b/schemas_json/server.yaml @@ -7,6 +7,8 @@ type: object required: - repositories properties: + log: + "$ref": "hatter://logging.yaml#" host: title: Host description: Listening host name @@ -40,7 +42,6 @@ definitions: required: - type - token - properties: type: enum: diff --git a/src_js/hatter/lenses.js b/src_js/hatter/lenses.js deleted file mode 100644 index 39da314..0000000 --- a/src_js/hatter/lenses.js +++ /dev/null @@ -1,21 +0,0 @@ -import R from 'ramda'; - - -export const index = R.lensIndex; - -export const prop = R.lensProp; - -export function path(...xs) { - return R.reduce((acc, i) => R.compose(acc, pathParamToLens(i)), - R.identity, xs); -} - -function pathParamToLens(x) { - switch (typeof(x)) { - case 'function': return x; - case 'number': return index(x); - case 'string': return prop(x); - case 'object': if (Array.isArray(x)) return R.apply(path, x); - } - throw 'Invalid path parameter'; -} diff --git a/src_js/hatter/main.js b/src_js/hatter/main.js index f5bee07..892b6af 100644 --- a/src_js/hatter/main.js +++ b/src_js/hatter/main.js @@ -2,7 +2,6 @@ import bean from 'bean'; import R from 'ramda'; import r from 'hatter/renderer'; -import * as l from 'hatter/lenses'; import * as common from 'hatter/common'; import * as vt from 'hatter/vt'; diff --git a/src_js/hatter/renderer.js b/src_js/hatter/renderer.js index f09d40f..aed7c48 100644 --- a/src_js/hatter/renderer.js +++ b/src_js/hatter/renderer.js @@ -1,126 +1,153 @@ -import Delegator from 'dom-delegator'; -import R from 'ramda'; -import bean from 'bean'; -import vh from 'virtual-dom/h'; -import diff from 'virtual-dom/diff'; -import patch from 'virtual-dom/patch'; -import createElement from 'virtual-dom/create-element'; - -import * as l from 'hatter/lenses'; - - -const delegator = Delegator(); -const vhTypes = ['VirtualNode', 'Widget']; - - -function vhFromArray(node) { - if (!node) - return []; - if (typeof node == 'string' || vhTypes.includes(node.type)) - return node; - if (!Array.isArray(node)) - throw 'Invalid node structure'; - if (node.length < 1) - return []; - if (typeof node[0] != 'string') - return node.map(vhFromArray); - let hasProps = (node.length > 1 && - typeof node[1] == 'object' && - !Array.isArray(node[1]) && - !vhTypes.includes(node[1].type)); - let children = R.flatten(node.slice(hasProps ? 2 : 1).map(vhFromArray)); - let result = hasProps ? vh(node[0], node[1], children) : - vh(node[0], children); - - // disable SoftSetHook for input - if (result.tagName == 'INPUT' && - result.properties && - result.properties.value && - typeof(result.properties.value) === 'object') { - result.properties.value = result.properties.value.value; - } - - return result; -} - - -class VTreeRenderer { - - constructor(el) { - this._el = el; - this._vtree = null; - } - - render(vtree) { - let vt = vhFromArray(vtree); - if (vt.type == 'VirtualNode') { - if (this._vtree) { - let d = diff(this._vtree, vt); - patch(this._el.firstChild, d); - } else { - while (this._el.firstChild) - this._el.removeChild(this._el.firstChild); - this._el.appendChild(createElement(vt)); - } - this._vtree = vt; - } else { - this._vtree = null; - while (this._el.firstChild) - this._el.removeChild(this._el.firstChild); - } - } - -} - - -export class Renderer { - - constructor(el, initState, vtCb) { - this.init(el, initState, vtCb); - } - - init(el, initState, vtCb) { - this._state = null; - this._changeCbs = []; - this._vtCb = vtCb; - this._r = new VTreeRenderer(el || document.querySelector('body')); - if (initState) - this.change(R.identity, _ => initState); - } - - view(...lenses) { - return R.view(R.apply(l.path, lenses), this._state); - } - - set(lens, value) { - if (arguments.length < 2) { - value = lens; - lens = R.identity; - } - this.change(lens, _ => value); - } - - change(lens, cb) { - if (arguments.length < 2) { - cb = lens; - lens = R.identity; - } - if (this._changeCbs.push(cb) > 1) - return; - let startingSubState = this.view(lens); - while (this._changeCbs.length > 0) { - this._state = R.over(l.path(lens), this._changeCbs[0], this._state); - this._changeCbs.shift(); - } - if (!this._vtCb || - (this._state && R.equals(startingSubState, this.view(lens)))) - return; - this._r.render(this._vtCb(this._state)); - bean.fire(this, 'render', this._state); - } - -} - - -const defaultRenderer = new Renderer(); -export default defaultRenderer; +import Delegator from 'dom-delegator'; +import bean from 'bean'; +import vh from 'virtual-dom/h'; +import diff from 'virtual-dom/diff'; +import patch from 'virtual-dom/patch'; +import createElement from 'virtual-dom/create-element'; + +import * as u from 'hatter/util'; + + +const delegator = Delegator(); +const vhTypes = ['VirtualNode', 'Widget']; + + +function vhFromArray(node) { + if (!node) + return []; + if (u.isString(node) || vhTypes.includes(node.type)) + return node; + if (!u.isArray(node)) + throw 'Invalid node structure'; + if (node.length < 1) + return []; + if (typeof node[0] != 'string') + return node.map(vhFromArray); + let hasProps = (node.length > 1 && + u.isObject(node[1]) && + !vhTypes.includes(node[1].type)); + let children = Array.from( + u.flatten(node.slice(hasProps ? 2 : 1).map(vhFromArray))); + let result = hasProps ? vh(node[0], node[1], children) : + vh(node[0], children); + return result; +} + + +class VTreeRenderer { + + constructor(el) { + this._el = el; + this._vtree = null; + } + + render(vtree) { + let vt = vhFromArray(vtree); + if (vt.type == 'VirtualNode') { + if (this._vtree) { + let d = diff(this._vtree, vt); + patch(this._el.firstChild, d); + } else { + while (this._el.firstChild) + this._el.removeChild(this._el.firstChild); + this._el.appendChild(createElement(vt)); + } + this._vtree = vt; + } else { + this._vtree = null; + while (this._el.firstChild) + this._el.removeChild(this._el.firstChild); + } + } + +} + + +export class Renderer { + + constructor(el, initState, vtCb, maxFps) { + this.init(el, initState, vtCb, maxFps); + } + + init(el, initState, vtCb, maxFps) { + this._state = null; + this._changes = []; + this._promise = null; + this._timeout = null; + this._lastRender = null; + this._vtCb = vtCb; + this._maxFps = maxFps; + this._r = new VTreeRenderer(el || document.querySelector('body')); + if (initState) + this.change(_ => initState); + } + + get(...paths) { + return u.get(paths, this._state); + } + + set(path, value) { + if (arguments.length < 2) { + value = path; + path = []; + } + return this.change(path, _ => value); + } + + change(path, cb) { + if (arguments.length < 2) { + cb = path; + path = []; + } + this._changes.push([path, cb]); + if (this._promise) + return this._promise; + this._promise = new Promise((resolve, reject) => { + setTimeout(() => { + try { + this._change(); + } catch(e) { + this._promise = null; + reject(e); + throw e; + } + this._promise = null; + resolve(); + }, 0); + }); + return this._promise; + } + + _change() { + let change = false; + while (this._changes.length > 0) { + let [path, cb] = this._changes.shift(); + let view = u.get(path); + let oldState = this._state; + this._state = u.change(path, cb, this._state); + if (this._state && u.equals(view(oldState), + view(this._state))) + continue; + change = true; + if (!this._vtCb || this._timeout) + continue; + let delay = (!this._lastRender || !this._maxFps ? + 0 : + (1000 / self._maxFps) - + (performance.now() - this._lastRender)); + this._timeout = setTimeout(() => { + this._timeout = null; + this._lastRender = performance.now(); + this._r.render(this._vtCb(this._state)); + bean.fire(this, 'render', this._state); + }, (delay > 0 ? delay : 0)); + } + if (change) + bean.fire(this, 'change', this._state); + } + +} + + +const defaultRenderer = new Renderer(); +export default defaultRenderer; diff --git a/src_js/hatter/util.js b/src_js/hatter/util.js new file mode 100644 index 0000000..e3dbfeb --- /dev/null +++ b/src_js/hatter/util.js @@ -0,0 +1,172 @@ + + +export const isArray = Array.isArray; +export const isObject = obj => obj !== null && + typeof(obj) == 'object' && + !isArray(obj); +export const isNumber = n => typeof(n) == 'number'; +export const isInteger = Number.isInteger; +export const isString = str => typeof(str) == 'string'; + + +export function clone(obj) { + if (isArray(obj)) + return Array.from(obj, clone); + if (isObject(obj)) { + let ret = {}; + for (let i in obj) + ret[i] = clone(obj[i]); + return ret; + } + return obj; +} + + +export function equals(x, y) { + if (x === y) + return true; + if (typeof(x) != 'object' || typeof(y) != 'object' || x === null || y === null) + return false; + if (Array.isArray(x) || Array.isArray(y)) { + if (!Array.isArray(x) || !Array.isArray(y) || x.length != y.length) + return false; + } + for (let i in x) + if (!equals(x[i], y[i])) + return false; + for (let i in y) + if (!equals(x[i], y[i])) + return false; + return true; +} + + +export function toPairs(obj) { + return Object.entries(obj); +} + + +export function fromPairs(arr) { + let ret = {}; + for (let [k, v] of arr) + ret[k] = v; + return ret; +} + + +export function* flatten(arr) { + if (isArray(arr)) { + for (let i of arr) + if (isArray(i)) + yield* flatten(i); + else + yield i; + } else { + yield arr; + } +} + + +export function pipe(...fns) { + if (fns.length < 1) + throw 'no functions'; + return function (...args) { + let ret = fns[0].apply(this, args); + for (let fn of fns.slice(1)) + ret = fn(ret); + return ret; + }; +} + + +export function curry(fn) { + let wrapper = function(oldArgs) { + return function(...args) { + args = oldArgs.concat(args); + if (args.length >= fn.length) + return fn.apply(this, args); + return wrapper(args); + }; + }; + return wrapper([]); +} + + +export const get = curry((path, obj) => { + let ret = obj; + for (let i of flatten(path)) { + if (ret === null || typeof(ret) != 'object') + return undefined; + ret = ret[i]; + } + return ret; +}); + + +export const change = curry((path, fn, obj) => { + function _change(path, obj) { + if (isInteger(path[0])) { + obj = (isArray(obj) ? Array.from(obj) : []); + } else if (isString(path[0])) { + obj = (isObject(obj) ? Object.assign({}, obj) : {}); + } else { + throw 'invalid path'; + } + if (path.length > 1) { + obj[path[0]] = _change(path.slice(1), obj[path[0]]); + } else { + obj[path[0]] = fn(obj[path[0]]); + } + return obj; + } + path = Array.from(flatten(path)); + if (path.length < 1) + return fn(obj); + return _change(path, obj); +}); + + +export const set = curry((path, val, obj) => change(path, _ => val, obj)); + + +export const omit = curry((path, obj) => { + function _omit(path, obj) { + if (isInteger(path[0])) { + obj = (isArray(obj) ? Array.from(obj) : []); + } else if (isString(path[0])) { + obj = (isObject(obj) ? Object.assign({}, obj) : {}); + } else { + throw 'invalid path'; + } + if (path.length > 1) { + obj[path[0]] = _omit(path.slice(1), obj[path[0]]); + } else { + delete obj[path[0]]; + } + return obj; + } + path = Array.from(flatten(path)); + if (path.length < 1) + return undefined; + return _omit(path, obj); +}); + + +export const sortBy = curry((fn, arr) => Array.from(arr).sort((x, y) => { + let xVal = fn(x); + let yVal = fn(y); + if (xVal < yVal) + return -1; + if (xVal > yVal) + return 1; + return 0; +})); + + +export const map = curry((fn, arr) => arr.map(fn)); + + +export const filter = curry((fn, arr) => arr.filter(fn)); + + +export const append = curry((val, arr) => arr.concat([val])); diff --git a/src_py/hatter/backend.py b/src_py/hatter/backend.py new file mode 100644 index 0000000..b2f97e4 --- /dev/null +++ b/src_py/hatter/backend.py @@ -0,0 +1,9 @@ + + +class Backend: + + def __init__(self, db_path): + pass + + async def async_close(self): + pass diff --git a/src_py/hatter/main.py b/src_py/hatter/main.py new file mode 100644 index 0000000..a110a4d --- /dev/null +++ b/src_py/hatter/main.py @@ -0,0 +1,96 @@ +import sys +import asyncio +import argparse +import pdb +import contextlib +import yaml +import logging.config +import atexit +import pkg_resources + +import hatter.json_validator +from hatter.backend import Backend +from hatter.server import create_web_server + + +def main(): + args = _create_parser().parse_args() + + with open(args.conf, encoding='utf-8') as conf_file: + conf = yaml.safe_load(conf_file) + hatter.json_validator.validate(conf, 'hatter://server.yaml#') + + if conf['log']: + logging.config.dictConfig(conf['log']) + + if args.web_path: + web_path = args.web_path + else: + atexit.register(pkg_resources.cleanup_resources) + web_path = pkg_resources.resource_filename('hatter', 'web') + + _run_until_complete_without_interrupt(async_main(conf, web_path)) + + +async def async_main(conf, web_path): + backend = None + web_server = None + try: + backend = Backend(conf.get('db_path', 'hatter.db')) + web_server = await create_web_server( + backend, conf.get('host', '0.0.0.0'), conf.get('port', 24000), + conf.get('webhook_path', '/webhook'), web_path) + await asyncio.Future() + except asyncio.CancelledError: + pass + except Exception as e: + pdb.set_trace() + raise + finally: + if web_server: + await web_server.async_close() + if backend: + await backend.async_close() + await asyncio.sleep(0.5) + + +def _create_parser(): + parser = argparse.ArgumentParser(prog='hatter') + parser.add_argument( + '--web-path', default=None, metavar='path', dest='web_path', + help="web ui directory path") + + named_arguments = parser.add_argument_group('required named arguments') + named_arguments.add_argument( + '--conf', required=True, metavar='path', dest='conf_path', + help='configuration path') + + return parser + + +def _run_until_complete_without_interrupt(future): + async def ping_loop(): + with contextlib.suppress(asyncio.CancelledError): + while True: + await asyncio.sleep(1) + + task = asyncio.ensure_future(future) + if sys.platform == 'win32': + ping_loop_task = asyncio.ensure_future(ping_loop()) + with contextlib.suppress(KeyboardInterrupt): + asyncio.get_event_loop().run_until_complete(task) + asyncio.get_event_loop().call_soon(task.cancel) + if sys.platform == 'win32': + asyncio.get_event_loop().call_soon(ping_loop_task.cancel) + while not task.done(): + with contextlib.suppress(KeyboardInterrupt): + asyncio.get_event_loop().run_until_complete(task) + if sys.platform == 'win32': + while not ping_loop_task.done(): + with contextlib.suppress(KeyboardInterrupt): + asyncio.get_event_loop().run_until_complete(ping_loop_task) + return task.result() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src_py/hatter/server.py b/src_py/hatter/server.py new file mode 100644 index 0000000..894d101 --- /dev/null +++ b/src_py/hatter/server.py @@ -0,0 +1,46 @@ +import asyncio +import aiohttp.web + + +async def create_web_server(backend, host, port, webhook_path, web_path): + srv = WebServer() + srv._backend = backend + srv._app = aiohttp.web.Application() + srv._app.router.add_route( + 'GET', '/', lambda req: aiohttp.web.HTTPFound('/index.html')) + srv._app.router.add_route('*', '/ws', srv._ws_handler) + srv._app.router.add_route('POST', webhook_path, srv._webhook_handler) + srv._app.router.add_static('/', web_path) + srv._app_handler = srv._app.make_handler() + srv._srv = await asyncio.get_event_loop().create_server( + srv._app_handler, host=host, port=port) + return srv + + +class WebServer: + + async def async_close(self): + self._srv.close() + await self._srv.wait_closed() + await self._app.shutdown() + await self._app_handler.finish_connections(0) + await self._app.cleanup() + + async def _ws_handler(self, request): + ws = aiohttp.web.WebSocketResponse() + await ws.prepare(request) + client = Client(self._backend, ws) + await client.run() + return ws + + async def _webhook_handler(self, request): + pass + + +class Client: + + def __init__(self, backend, ws): + pass + + async def run(self): + pass diff --git a/webpack.config.js b/webpack.config.js index a76c0af..097b089 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,4 @@ var path = require('path'); -var fs = require('fs'); -var webpack = require('webpack'); module.exports = [ @@ -14,24 +12,14 @@ module.exports = [ }, module: { rules: [ - { - test: /\.js$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader' - } - }, { test: /\.scss$/, use: ["style-loader", "css-loader", "resolve-url-loader", "sass-loader?sourceMap"] }, - - // TODO isolate fonts only - { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, use: "url-loader?name=fonts/[hash].[ext]&limit=10000&mimetype=application/font-woff" }, - { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, use: "url-loader?name=fonts/[hash].[ext]&limit=10000&mimetype=application/font-woff" }, - { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: "url-loader?name=fonts/[hash].[ext]&limit=10000&mimetype=application/octet-stream" }, - { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: "file-loader?name=fonts/[hash].[ext]" }, - { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: "url-loader?name=fonts/[hash].[ext]&limit=10000&mimetype=image/svg+xml" } + { + test: /node_modules.*\.(woff|woff2|ttf|eot|svg)$/, + use: "file-loader?name=fonts/[hash].[ext]" + } ] }, resolve: { -- cgit v1.2.3-70-g09d2