From ed9f4f8991f4e8b31270dbbf17215732d73c9e37 Mon Sep 17 00:00:00 2001 From: bozokopic Date: Mon, 24 Apr 2017 23:08:48 +0200 Subject: simple web app --- playground/server/run.sh | 4 ++ src_js/opcut/common.js | 3 ++ src_js/opcut/lenses.js | 21 ++++++++ src_js/opcut/main.js | 18 ++++++- src_js/opcut/renderer.js | 126 ++++++++++++++++++++++++++++++++++++++++++++++ src_js/opcut/vt.js | 8 +++ src_py/opcut/doit/main.py | 4 +- src_py/opcut/main.py | 80 +++++++++++++++++++++++++++++ src_py/opcut/util.py | 40 +++++++++++++++ src_web/static/index.html | 10 ++++ src_web/static/main.html | 10 ---- 11 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 playground/server/run.sh create mode 100644 src_js/opcut/common.js create mode 100644 src_js/opcut/lenses.js create mode 100644 src_js/opcut/renderer.js create mode 100644 src_js/opcut/vt.js create mode 100644 src_py/opcut/main.py create mode 100644 src_py/opcut/util.py create mode 100644 src_web/static/index.html delete mode 100644 src_web/static/main.html diff --git a/playground/server/run.sh b/playground/server/run.sh new file mode 100644 index 0000000..5b7cc98 --- /dev/null +++ b/playground/server/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +export PYTHONPATH=../../src_py +python -m opcut.main --ui-path ../../build/jsopcut diff --git a/src_js/opcut/common.js b/src_js/opcut/common.js new file mode 100644 index 0000000..adc7191 --- /dev/null +++ b/src_js/opcut/common.js @@ -0,0 +1,3 @@ + + +export const defaultState = {}; diff --git a/src_js/opcut/lenses.js b/src_js/opcut/lenses.js new file mode 100644 index 0000000..8458dbd --- /dev/null +++ b/src_js/opcut/lenses.js @@ -0,0 +1,21 @@ +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/opcut/main.js b/src_js/opcut/main.js index b7850ca..b4da90b 100644 --- a/src_js/opcut/main.js +++ b/src_js/opcut/main.js @@ -1,3 +1,19 @@ +import bean from 'bean'; +import R from 'ramda'; -import 'static!static/main.html'; +import r from 'opcut/renderer'; +import * as l from 'opcut/lenses'; +import * as common from 'opcut/common'; +import * as vt from 'opcut/vt'; + +import 'static!static/index.html'; import 'style/main.scss'; + + +function main() { + let root = document.body.appendChild(document.createElement('div')); + r.init(root, common.defaultState, vt.main); +} + + +bean.on(window, 'load', main); diff --git a/src_js/opcut/renderer.js b/src_js/opcut/renderer.js new file mode 100644 index 0000000..22a74c6 --- /dev/null +++ b/src_js/opcut/renderer.js @@ -0,0 +1,126 @@ +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 'opcut/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; diff --git a/src_js/opcut/vt.js b/src_js/opcut/vt.js new file mode 100644 index 0000000..e199bbe --- /dev/null +++ b/src_js/opcut/vt.js @@ -0,0 +1,8 @@ + + +export function main() { + return ['div', + 'application in development - ', + ['a', {href: 'https://github.com/bozokopic/opcut'}, 'Github page'] + ]; +} diff --git a/src_py/opcut/doit/main.py b/src_py/opcut/doit/main.py index 92e4fc9..d45859b 100644 --- a/src_py/opcut/doit/main.py +++ b/src_py/opcut/doit/main.py @@ -39,7 +39,9 @@ def task_dist_clean(): def task_dist_build(): """Distribution - build (DEFAULT)""" - return {'actions': [(_common.mkdir_p, ['dist'])], + return {'actions': [(_common.rm_rf, ['dist']), + (_common.cp_r, ['build/pyopcut', 'dist']), + (_common.cp_r, ['build/jsopcut', 'dist/web'])], 'task_dep': [ 'gen_all', 'pyopcut_build', diff --git a/src_py/opcut/main.py b/src_py/opcut/main.py new file mode 100644 index 0000000..d6b42a9 --- /dev/null +++ b/src_py/opcut/main.py @@ -0,0 +1,80 @@ +import sys +import argparse +import yaml +import logging.config +import urllib.parse +import aiohttp.web +import ssl +import asyncio +import contextlib + +from opcut import util +import opcut.json_validator + + +def main(): + args = _create_parser().parse_args() + + if args.log_conf_path: + with open(args.log_conf_path, encoding='utf-8') as log_conf_file: + log_conf = yaml.safe_load(log_conf_file) + opcut.json_validator.validate(log_conf, 'opcut://logging.yaml#') + logging.config.dictConfig(log_conf) + + util.run_until_complete_without_interrupt(async_main(args)) + + +async def async_main(args): + + addr = urllib.parse.urlparse(args.ui_addr) + + if addr.scheme == 'https': + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ssl_ctx.load_cert_chain(args.ui_pem_path) + else: + ssl_ctx = None + + app = aiohttp.web.Application() + app.router.add_route('GET', '/', + lambda req: aiohttp.web.HTTPFound('/index.html')) + app.router.add_static('/', args.ui_path) + app_handler = app.make_handler() + + srv = await asyncio.get_event_loop().create_server( + app_handler, host=addr.hostname, port=addr.port, ssl=ssl_ctx) + + with contextlib.suppress(asyncio.CancelledError): + await asyncio.Future() + + srv.close() + await srv.wait_closed() + await app.shutdown() + await app_handler.finish_connections(0) + await app.cleanup() + + +def _create_parser(): + parser = argparse.ArgumentParser(prog='opcut') + parser.add_argument( + '--ui-addr', default='http://0.0.0.0:8080', + metavar='addr', dest='ui_addr', + help="address of listening web ui socket formated as " + "'://:' - is 'http' or 'https'; " + " is hostname; is tcp port number " + "(default http://0.0.0.0:8080)") + parser.add_argument( + '--ui-path', default='web', + metavar='path', dest='ui_path', + help="web front-end path (default web)") + parser.add_argument( + '--ui-pem', default=None, + metavar='path', dest='ui_pem_path', + help="web front-end pem file path - required for https") + parser.add_argument( + '--log', default=None, metavar='path', dest='log_conf_path', + help="logging configuration") + return parser + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src_py/opcut/util.py b/src_py/opcut/util.py new file mode 100644 index 0000000..4855bc5 --- /dev/null +++ b/src_py/opcut/util.py @@ -0,0 +1,40 @@ +import contextlib +import asyncio +import sys + + +def run_until_complete_without_interrupt(future): + """Run event loop until future or coroutine is done + + Args: + future (Awaitable): future or coroutine + + Returns: + Any: provided future's result + + KeyboardInterrupt is suppressed (while event loop is running) and is mapped + to single cancelation of running task. If multipple KeyboardInterrupts + occur, task is canceled only once. + + """ + 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() diff --git a/src_web/static/index.html b/src_web/static/index.html new file mode 100644 index 0000000..f6ff4eb --- /dev/null +++ b/src_web/static/index.html @@ -0,0 +1,10 @@ + + + + + opcut + + + + + diff --git a/src_web/static/main.html b/src_web/static/main.html deleted file mode 100644 index f6ff4eb..0000000 --- a/src_web/static/main.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - opcut - - - - - -- cgit v1.2.3-70-g09d2