# HG changeset patch # User Tero Marttila # Date 1402147299 -10800 # Node ID e5799432071c8dc694b0ae2fe95d101cde130b0b # Parent 292b26405ee7b7efeb1bbef19d11b4d2e511edb8 qmsk.web: port pvl.web to python3, and rewrite html diff -r 292b26405ee7 -r e5799432071c qmsk/web/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qmsk/web/__init__.py Sat Jun 07 16:21:39 2014 +0300 @@ -0,0 +1,9 @@ +# Modules +from qmsk.web import args, urls + +# Classes +from qmsk.web.application import Application, Handler +#from qmsk.web.html import HTMLHandler + +# Objects +#from qmsk.web.html import html5 diff -r 292b26405ee7 -r e5799432071c qmsk/web/application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qmsk/web/application.py Sat Jun 07 16:21:39 2014 +0300 @@ -0,0 +1,138 @@ +from werkzeug.wrappers import Request, Response +from werkzeug.exceptions import ( + HTTPException, + BadRequest, # 400 + NotFound, # 404 +) +from werkzeug.utils import redirect + +class Application: + URLS = None + + def __init__ (self, urls=None): + """ + urls - werkzeug.routing.Map -> Handler + """ + + if not urls: + urls = self.URLS + + if not urls: + raise ValueError("Required URLS/urls=...") + + self.urls = urls + + def respond (self, request): + """ + Lookup Request -> Handler, params -> Response + """ + + # bind to request + urls = self.urls.bind_to_environ(request) + + # lookup + handler, params = urls.match() + + # handler instance + handler = handler(self, request, urls) + + try : + handler.init() + + # apply + return handler.respond(**params) + + finally : + handler.cleanup() + + @Request.application + def __call__ (self, request) : + """ + WSGI entry point, werkzeug Request -> Response + """ + + try : + return self.respond(request) + + except HTTPException as ex : + return ex + +class Handler (object) : + """ + Per-Request controller/view, containing the request context and generating the response. + """ + + # werkzeug defaults to UTF-8 + MIMETYPE = 'text/html' + + def __init__ (self, app, request, urls) : + """ + app - wsgi.Application + request - werkzeug.Request + urls - werkzeug.routing.Map.bind_to_environ() + """ + + self.app = app + self.request = request + self.urls = urls + + def url (self, handler=None, **params) : + """ + Return an URL for given endpoint, with parameters, + """ + + if not handler : + handler = self.__class__ + + return self.urls.build(handler, params) + + ## processing stages + def init (self) : + """ + Initialize on request start. + """ + + pass + + def process (self, **params) : + """ + Process request args to build internal request state. + + May optionally return a Response, to e.g. redirect after POST. + """ + + pass + + def render (self) : + """ + Render response. + """ + + raise NotImplementedError() + + def mimetype (self): + return self.MIMETYPE + + def status (self): + return 200 + + def respond (self, **params) : + """ + Generate a response, or raise an HTTPException + """ + + # returning e.g. redirect? + response = self.process(**params) + + if response : + return response + + return Response(self.render_response(), mimetype=self.mimetype(), status=self.status()) + + def cleanup (self) : + """ + After request processing. Do not fail :) + """ + + pass + diff -r 292b26405ee7 -r e5799432071c qmsk/web/args.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qmsk/web/args.py Sat Jun 07 16:21:39 2014 +0300 @@ -0,0 +1,46 @@ +import werkzeug.serving +import argparse + +import logging; log = logging.getLogger('qmsk.web.args') + +def options (parser, static=None) : + """ + Command-line options. + """ + + parser = parser.add_argument_group("qmsk.web") + + parser.add_argument('--web-static', metavar='PATH', default=static, + help="Path to static files") + + parser.add_argument('--web-debug', action='store_true', + help="Web-based debugger") + + return parser + +def apply (options, application_class, *args, **opts): + """ + Build given qmsk.web.Application from options. + """ + + return application_class(*args, + **opts + ) + +def main (options, application) : + """ + Run given WSGI application via the werkzeug development server. + """ + + static_files = { } + + if options.web_static: + static_files['/static'] = options.web_static + + log.info("http://0.0.0.0:8080/") + werkzeug.serving.run_simple('0.0.0.0', 8080, application, + use_debugger = options.web_debug, + static_files = static_files, + ) + + return 0 diff -r 292b26405ee7 -r e5799432071c qmsk/web/html.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qmsk/web/html.py Sat Jun 07 16:21:39 2014 +0300 @@ -0,0 +1,188 @@ +from qmsk.utils import flatten, merge +from html import escape + +import qmsk.web + +class Tag : + def __init__ (self, _name=None, *_contents, + _selfclosing=True, + _whitespace_sensitive=False, + **_attrs) : + self.name = _name + self.contents = _contents or [ ] + self.attrs = _attrs or { } + + # options + self.selfclosing = _selfclosing + self.whitespace_sensitive = _whitespace_sensitive + + def __call__ (self, *_contents, + _selfclosing=None, + _whitespace_sensitive=None, + **_attrs): + return type(self)(self.name, *tuple(flatten(self.contents, _contents)), + _selfclosing = self.selfclosing if _selfclosing is None else _selfclosing, + _whitespace_sensitive = self.whitespace_sensitive if _whitespace_sensitive is None else _whitespace_sensitive, + **merge(self.attrs, _attrs) + ) + + def render_attrs (self): + for name, value in self.attrs.items(): + name = name.strip('_').replace('_', '-') + + if value is True: + value = name + elif value is None: + continue + else: + value = str(value) + + yield '{name}="{value}"'.format(name=name, value=escape(value, quote=True)) + + def render (self, indent): + """ + Iterate over lines of output HTML. + """ + + open = (self.contents or not self.selfclosing) + + if self.name and self.attrs and open: + yield indent, '<{name} {attrs}>'.format(name=self.name, attrs=' '.join(self.render_attrs())) + elif self.name and self.attrs: + yield indent, '<{name} {attrs} />'.format(name=self.name, attrs=' '.join(self.render_attrs())) + elif self.name and open: + yield indent, '<{name}>'.format(name=self.name) + elif self.name: + yield indent, '<{name} />'.format(name=self.name) + + for item in self.contents: + if isinstance(item, Tag): + yield from item.render(indent=indent+1) + elif self.whitespace_sensitive: + yield 0, str(item) + else: + yield indent + 1, str(item) + + if self.name and open: + yield indent, ''.format(name=self.name) + + def __str__ (self): + """ + Render as HTML. + """ + + return '\n'.join('\t'*indent+line for indent, line in self.render(indent=0)) + + def __repr__ (self): + return '{name}({args})'.format( + name = self.__class__.__name__, + args = ', '.join( + [repr(self.name)] + + [repr(item) for item in self.contents] + + ['{name}={value!r}'.format(name=name, value=value) for name, value in self.attrs.items()] + ), + ) + +class HTML: + pre = Tag('pre', _whitespace_sensitive=True) + + def __getattr__ (self, name): + """ + Get an empty Tag object. + """ + + return Tag(name) + + def __call__ (self, *values): + """ + Raw HTML. + """ + return Tag(None, *values) + +class HTML5 (HTML): + span = Tag('span', _selfclosing=False) + script = Tag('script', _selfclosing=False) + +html5 = HTML5() + +class HTMLHandler (qmsk.web.Handler): + """ + A handler that renders a full HTML page. + """ + + # HTML5 + html = html5 + + DOCTYPE = 'html' + HTML_XMLNS = None + HTML_LANG = 'en' + + # + TITLE = None + STYLE = None + SCRIPT = None + CSS = ( + + ) + JS = ( + + ) + HEAD = None + + def title (self): + return self.TITLE + + def render (self): + raise NotImplementedError() + + def render_html (self) : + """ + Render HTML tag. + """ + + html = self.html + + return html.html( + html.head( + html.title(self.title()), + ( + html.link(rel='Stylesheet', type="text/css", href=src) for src in self.CSS + ), + ( + html.script(src=src, type='text/javascript', _selfclosing=False) for src in self.JS + ), + html.style(type='text/css')(self.STYLE) if self.STYLE else None, + html.script(type='text/javascript')(self.SCRIPT) if self.SCRIPT else None, + self.HEAD, + ), + html.body( + self.render(), + ), + xmlns=self.HTML_XMLNS, lang=self.HTML_LANG) + + def render_response (self): + """ + Render entire HTML response. + """ + + return """\ + +{html}\ +""".format(doctype=self.DOCTYPE, html=self.render_html()) + +if __name__ == '__main__': + html = HTML5() + + print(html.html( + html.head( + html.title("Testing") + ), + html.body( + html.h1("Testing"), + html.p("Just testing this..."), + html("Raw HTML "), + html.pre(repr( + html.a(href="/foo")("Foo!") + )) + ), + )) diff -r 292b26405ee7 -r e5799432071c qmsk/web/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qmsk/web/urls.py Sat Jun 07 16:21:39 2014 +0300 @@ -0,0 +1,10 @@ +from werkzeug.routing import Map, Rule + +def rules (rules): + """ + >>> rules({'/foo': Foo}) + """ + + return Map([ + Rule(rule, endpoint=endpoint) for rule, endpoint in rules.items() + ])