WSGIアプリからDBSprocketsを使う

先日作ったプロキシサーバに管理画面を付けたくなったので、前から気になっていたDBSprocketsを素のWSGIから使えるようにしてみた。

Pylons、TG2には依存せずにDBSproketsDBMechanicの全機能が一応使えるはず。

  • dbmechanic.py
"""
dbmechanic module for WSGI

Initial version by Nozomu Kaneko <nozom.kaneko _at_ gmail {d0t} com> based on
the Pylons version.

"""
import pkg_resources
from paste.httpexceptions import HTTPException, HTTPFound
from paste.registry import StackedObjectProxy
from toscawidgets.api import Widget, WidgetBunch
from genshi.template import TemplateLoader
from sqlalchemy.exceptions import IntegrityError, ProgrammingError
from dbsprockets.sprockets import Sprockets
from webob import Request
from selector import Selector

__all__ = ['DBMechanic', 'c']


c = StackedObjectProxy(name="C")

def render_genshi(template_name, genshi_loader, **kw):
    template = genshi_loader.load(template_name)
    return template.generate(c=c, **kw).render()


def flash(environ, msg, status=None):
    environ["flash.message"] = msg
    environ["flash.status"] = status or "status_ok"

def get_flash(environ):
    msg = environ.get('flash.message', '')
    environ['flash.message'] = ''
    return msg

def get_status(environ):
    status = environ.get('flash.status', '')
    environ['flash.status'] = ''
    return status


def redirect_to(location):
    headers = [('location', location)]
    raise HTTPFound(headers=headers)

def message_from_exception(e):
    message = None
    if hasattr(e,"message"):
        message = e.message
    if isinstance(message,str):
        try:
            message = message.decode('utf-8')
        except:
            message = None
    return message


class ContextObj(object):
    pass


class DBMechanic(object):
    sprockets = None

    def __init__(self, provider, controller, *args, **kwargs):

        self.provider = provider
        self.sprockets = Sprockets(provider, controller)
        self.controller = controller

        self.genshi_loader = TemplateLoader([
            pkg_resources.resource_filename('dbsprockets.dbmechanic.frameworks.pylons', 'templates'),
        ])

        selector = Selector(consume_path=False)
        selector.add('/', GET=self.index)
        selector.add('/tableView/{tableName}', GET=self.tableView)
        selector.add('/tableDef/{tableName}', GET=self.tableDef)
        selector.add('/addRecord/{tableName}', GET=self.addRecord)
        selector.add('/editRecord/{tableName}', GET=self.editRecord)
        selector.add('/add', POST=self.add)
        selector.add('/edit', POST=self.edit)
        selector.add('/delete/{tableName}', GET=self.delete)
        self.app = selector

        sprocket = self.sprockets['databaseView']
        self.databaseValue = sprocket.session.getValue()
        self.databaseView  = sprocket.view.widget
        self.databaseDict  = dict(controller=self.controller)

    def __call__(self, environ, start_response):
        # setup app env
        registry = environ['paste.registry']
        registry.register(c, ContextObj())

        try:
            return self.app(environ, start_response)
        except HTTPException, http_exc:
            resp = http_exc.response(environ)
            return resp(environ, start_response)

    def index(self, environ, start_response):
        c.w = WidgetBunch()
        c.w.databaseView  = self.databaseView
        c.w.mainView = Widget("widget")

        # flash(environ, "Welcome!")

        start_response('200 OK', [('Content-Type', 'text/html')])
        return render_genshi('index.html', self.genshi_loader,
                             tg_flash=get_flash(environ),
                             tg_status=get_status(environ))

    def tableView(self, environ, start_response):
        request = Request(environ)
        tableName = request.urlvars['tableName']
        c.tableName = tableName

        c.page = request.params.get('page', 1)
        c.recordsPerPage = request.params.get('recordsPerPage', 25)
        #this should probably be a decorator
        c.page = int(c.page)
        c.recordsPerPage = int(c.recordsPerPage)

        sprocket  = self.sprockets['tableView__'+tableName]
        c.w = WidgetBunch()
        c.w.mainView  = sprocket.view.widget
        c.w.databaseView  = self.databaseView
        c.mainValue = sprocket.session.getValue(values=request.params,
                                                page=c.page,
                                                recordsPerPage=c.recordsPerPage)
        c.mainCount = sprocket.session.getCount(values=request.params)
        for key, value in self.databaseDict.iteritems():
            setattr(c, key, value)

        start_response('200 OK', [('Content-Type', 'text/html')])
        return render_genshi('tableView.html', self.genshi_loader,
                             tg_flash=get_flash(environ),
                             tg_status=get_status(environ))

    def tableDef(self, environ, start_response):
        request = Request(environ)
        tableName = request.urlvars['tableName']
        c.tableName = tableName

        sprocket  = self.sprockets['tableDef__'+tableName]
        c.w = WidgetBunch()
        c.w.mainView  = sprocket.view.widget
        c.w.databaseView  = self.databaseView
        c.mainValue = sprocket.session.getValue()
        for key, value in self.databaseDict.iteritems():
            setattr(c, key, value)

        start_response('200 OK', [('Content-Type', 'text/html')])
        return render_genshi('index.html', self.genshi_loader,
                             tg_flash=get_flash(environ),
                             tg_status=get_status(environ))

    def addRecord(self, environ, start_response):
        request = Request(environ)
        tableName = request.urlvars['tableName']
        c.tableName = tableName

        sprocket = self.sprockets['addRecord__'+tableName]
        c.w = WidgetBunch()
        c.w.mainView  = sprocket.view.widget
        c.w.databaseView  = self.databaseView
        kw = {}
        for key, value in request.params.iteritems():
            if key and value:
                kw[key] = value
        c.mainValue = sprocket.session.getValue(values=kw)
        for key, value in self.databaseDict.iteritems():
            setattr(c, key, value)

        start_response('200 OK', [('Content-Type', 'text/html')])
        return render_genshi('index.html', self.genshi_loader,
                             tg_flash=get_flash(environ),
                             tg_status=get_status(environ))

    def editRecord(self, environ, start_response):
        request = Request(environ)
        tableName = request.urlvars['tableName']
        c.tableName = tableName

        sprocket = self.sprockets['editRecord__'+tableName]
        c.w = WidgetBunch()
        c.w.mainView  = sprocket.view.widget
        c.w.databaseView  = self.databaseView
        kw = {}
        for key, value in request.params.iteritems():
            if key and value:
                kw[key] = value
        c.mainValue = sprocket.session.getValue(values=kw)
        for key, value in self.databaseDict.iteritems():
            setattr(c, key, value)

        start_response('200 OK', [('Content-Type', 'text/html')])
        return render_genshi('index.html', self.genshi_loader,
                             tg_flash=get_flash(environ),
                             tg_status=get_status(environ))

    def add(self, environ, start_response):
        request = Request(environ)
        tableName = request.params['tableName']
        params = request.POST.copy()

        try:
            self._createRelationships(tableName, params)
            self.provider.add(tableName, values=params)
        except (IntegrityError, ProgrammingError), e:
            message = message_from_exception(e)
            if message:
                flash(environ, message, "status_alert")
            request.urlvars['tableName'] = tableName
            return self.addRecord(environ, start_response)

        redirect_to(self.controller + '/tableView/' + tableName)

    def edit(self, environ, start_response):
        request = Request(environ)

        tableName = request.params['tableName']
        params = request.POST.copy()

        try:
            self._createRelationships(tableName, params)
            self.provider.edit(tableName, values=params)
        except (IntegrityError, ProgrammingError), e:
            message = message_from_exception(e)
            if message:
                flash(environ, message, "status_alert")
            return self.editRecord(environ, start_response)

        redirect_to(self.controller + '/tableView/' + tableName)

    def delete(self, environ, start_response):
        request = Request(environ)
        tableName = request.urlvars['tableName']
        params = request.GET.copy()

        self.provider.delete(tableName, values=params)
        redirect_to(self.controller + '/tableView/' + tableName)

    def _createRelationships(self, tableName, params):
        #this might become a decorator
        #check to see if the table is a many-to-many table first
        if tableName in self.provider.getManyToManyTables():
            return
        #right now many-to-many only supports single primary keys
        id = params[self.provider.getPrimaryKeys(tableName)[0]]
        relationships = {}
        for key, value in params.iteritems():
            if key.startswith('many_many_'):
                relationships.setdefault(key[10:], []).append(value)
        for key, value in relationships.iteritems():
            self.provider.setManyToMany(tableName, id, key, value)

WSGIProxyで使うには、wsgiapp.pyに以下の変更を行う。

+import pkg_resources
 from paste.proxy import TransparentProxy
+from paste.cascade import Cascade
+from paste.urlparser import StaticURLParser
+from paste.registry import RegistryManager
+from paste.deploy.converters import aslist
+from toscawidgets.middleware import TGWidgetsMiddleware
+from toscawidgets.mods.wsgi import WSGIHostFramework
+from genshi.template import TemplateLoader
 from sqlalchemy import engine_from_config
+import elixir
+from dbsprockets.sprockets import SAProvider
 
 import model
+from dbmechanic import DBMechanic
 
 
 class ProxyAdmin(object):
 
+    def __init__(self, controller_hosts):
+	self.controller_hosts = controller_hosts
+
+        dbmechanic = DBMechanic(SAProvider(elixir.metadata), '')
+        dbmechanic.genshi_loader = TemplateLoader([
+            pkg_resources.resource_filename('wsgiproxy', 'templates'),
+        ])
+        # Static files
+        staticapp = StaticURLParser(
+            pkg_resources.resource_filename('wsgiproxy', 'public'))
+        self.dbmechanic = Cascade([staticapp, dbmechanic])
+
     def __call__(self, environ, start_response):
+        if environ["HTTP_HOST"] in self.controller_hosts:
+            return self.dbmechanic(environ, start_response)
         query = model.HostNameMapping.query.filter_by(enabled=True)
         mapping = query.filter_by(hostname=environ["HTTP_HOST"]).first()
         if mapping:
             proxy = TransparentProxy(force_host=mapping.mapped)
         else:
             proxy = TransparentProxy()
         return proxy(environ, start_response)
 
 
-def make_app(global_conf, **kw):
+def make_app(global_conf, controller_hosts="", **kw):
     # Here we merge all the keys into one configuration
     # dictionary; you don't have to do this, but this
     # can be convenient later to add ad hoc configuration:
     conf = global_conf.copy()
     conf.update(kw)
 
     # Setup model
     model.metadata.bind = engine_from_config(conf, 'sqlalchemy.')
     model.setup_all()
     # model.create_all()
 
     # This is a WSGI application:
-    app = ProxyAdmin()
+    app = ProxyAdmin(aslist(controller_hosts))
+
+    # Setup ToscaWidgets
+    app = TGWidgetsMiddleware(app, WSGIHostFramework())
+
+    # Establish the Registry for this application
+    app = RegistryManager(app)
 
     return app

development.iniに1行追加する。

 [app:devel]
 # This application is meant for interactive development
 use = egg:TinyWSGIProxy
 
 sqlalchemy.url = sqlite:////%(here)s/data.db
+controller_hosts = admin p.p # config.privoxy.org

DBSprocketsに含まれるPylons用のテンプレート(dbsprockets/dbmechanic/frameworks/pylons/templates)は、そのままではうまく動かなかったので、以下の修正を行った。

--- index.html	2008-03-19 04:04:00.000000000 +0900
+++ index.html	2008-03-23 05:44:50.000000000 +0900
@@ -8,7 +8,7 @@
 <body>
       <div class="tableList" style="background-color:#aaddff; float:left; clear:left; border:1px solid; margin:0px 5px 0px 0px; padding:10px;">
             <b>Tables</b>
-            ${c.w.databaseView(value=databaseValue, controller=controller)}
+            ${Markup(c.w.databaseView(value=databaseValue, controller=controller))}
       </div>
       <div class="mainView" style="bg-color:#ffffff;">
             <py:if test="not isinstance(tableName, Undefined)">
@@ -22,7 +22,7 @@
             </py:if>
                   
             <py:if test="mainView is not None">
-                  ${c.w.mainView(value=mainValue)}
+                  ${Markup(c.w.mainView(value=c.mainValue) or '')}
             </py:if>
       </div>
 </body>
--- tableView.html	2008-03-19 04:04:00.000000000 +0900
+++ tableView.html	2008-03-23 05:46:11.000000000 +0900
@@ -8,7 +8,7 @@
 <body>
       <div class="tableList" style="background-color:#aaddff; float:left; clear:left; border:1px solid; margin:0px 5px 0px 0px; padding:10px;">
             <b>Tables</b>
-            ${c.w.databaseView(value=c.databaseValue, controller=c.controller)}
+            ${Markup(c.w.databaseView(value=c.databaseValue, controller=c.controller))}
       </div>
       <div class="mainView" style="bg-color:#ffffff;">
             <py:if test="not isinstance(c.tableName, Undefined)">
@@ -74,9 +74,9 @@
             </div>
             <div style="float:clear"><br/></div>
             <py:if test="mainView is not None">
-                  ${c.w.mainView(value=c.mainValue)}
+                  ${Markup(c.w.mainView(value=c.mainValue))}
             </py:if>
-            <div style="float:clear"/>
+            <div style="float:clear"> </div>
             <div style="float:right">
                   Records Per Page:
                   <a href="${c.controller}/tableView/$c.tableName?page=1&amp;recordsPerPage=10">10</a> 

数カ所HTMLがエスケープされてしまう部分があったのでMarkup()で囲むようにした。最後の変更はデザインが崩れてしまうため。

あと、以下は必須ではないけど気分の問題で変更してみた。

--- master.html	2008-03-19 04:04:00.000000000 +0900
+++ master.html	2008-03-23 05:40:48.000000000 +0900
@@ -7,7 +7,7 @@
 <xi:include href="tw_resources.html" />
 <head py:match="head" py:attrs="select('@*')">
     <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
-    <title py:replace="''">Your title goes here</title>
+    <title>WSGIProxy Admin</title>
 	<link py:for="css in tg_css" py:replace="ET(css.display())" />
     <link py:for="js in tg_js_head" py:replace="ET(js.display())" />
     <meta py:replace="select('*')"/>
@@ -25,7 +25,7 @@
   <!-- End of main_content -->
   </div>
   <div id="footer"> 
-  	<p>Pylons under the hood!</p>
+  	<p>WSGI under the hood!</p>
   </div>
 </body>
 

これだけでも動作するのだけど、やっぱりデザインが入っていないと寂しいので、TurboGearsのプロジェクトテンプレートからpublicディレクトリを丸ごとコピーしてきて、最終的にはこんな感じになった。


参考にしたページ