--- /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()
+ ])