qmsk.web: port pvl.web to python3, and rewrite html
authorTero Marttila <terom@paivola.fi>
Sat, 07 Jun 2014 16:21:39 +0300
changeset 92 e5799432071c
parent 91 292b26405ee7
child 93 7ee3a6608406
qmsk.web: port pvl.web to python3, and rewrite html
qmsk/web/__init__.py
qmsk/web/application.py
qmsk/web/args.py
qmsk/web/html.py
qmsk/web/urls.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
--- /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
+
--- /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
--- /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, '</{name}>'.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'
+
+    # <head>
+    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 <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 """\
+<!DOCTYPE {doctype}>
+{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 <tags>"),
+            html.pre(repr(
+                html.a(href="/foo")("Foo!")
+            ))
+        ),
+    ))
--- /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()
+    ])