DragonPy -- 又一个新的webapp架构,它是面向python程序员的,包含了 Karrigell 和 Quixote,大家一起来造这个轮子(LiHui)。

::-- ZoomQuiet [DateTime(2005-07-13T02:41:38Z)] TableOfContents

现有 Web Application 点评

这么多Web Application,到底用什么好啊

俺以前一直用zope,后来感觉是越来越差,原因与Quixote和Cherrypy的作者差不多,网上有他们的文章,俺这里就不说了。后来找啊找啊,前前后后重点试过Quixote、Cherrypy、Karrigell,感觉都还不错,各有各的特点:

Cherrypy:将对象机制引入web开发,完全的面向对象发布机制,底层数据、api函数使用cpg集中管理。缺点是底层api调用代码老在变,天啊,俺真的受不了,以前做了一个cherrypy小应用,前前后后重构了10来遍,每次只要用svn更新一下cherrypy代码,就得改一次俺的应用代码。俺就搞不明白,比如一个调用session,就用cpg.sessions就成了吧,为什么一会儿这个样式,一会儿另外个样式。

Quixote:不错,发布方式多,代码结构好,质量最佳,基本上没什么BUG。只是语法调用有点怪异,适应了半天,最后还是不适应。

Karrigell:最易用的系统,入门真简便,良好的debug框架,动态载入。只是代码结构不好,做个应用就得将Karrigell移过去。再有url不支持定制,太有局限性,比如我动态生成一个js文件,总不能扩展名是个py,hip...吧,而且作者象俺有点不务正业,不老老实实做好框架,却去设计什么dbstrange,还做了一堆没用的半成品,唉,有这功夫把自留地种好啊。感觉做做原型、快速开发还不错。

PS:这两天谈的比较多的是ruby的web架构,不太感冒,这东东也就是让java程序员见识一下:哦,这么这么简单啊,其实不用编码的框架是不存在的,功能实现与编码量本身就是一对对头。

需求

见识了这么多框架,俺理想的架构长得模样也自然产生了(主要性能):

1)具有多多的发布接口,象Quixote,就是俺以前提过的万能接口,接口越多,余地越大啊。

2)底层API与数据的集中管理与调用。比如dgn.get_request(),dgn.get_reponse(),这里的特性是Quixote与Cherrypy的结合和升华。

3)简便的发布方式。考虑web前端部份的python代码是最易变的,尽可能简单,易读。利用所有都是对象的性能,选择函数发布。

4)支持url定制,易url的default性能。这个需求好象现在很重要,总比使用apache的mod_write好多了。

前题

系统使用Karrigell和Quixote代码,对于这两个系统的补丁修改,一律使用动态修正方式偷梁换柱,不改变系统的任何代码,以满足原系统的升级和版权等要求。

一个Hello world模块

还是用一个简便例子说明:

   1 #--系统函数--
   2 def _charsetGB2312(fn):
   3     '''函数包装函数,将页面设为gb2312'''
   4     def _init(dgn,*args):
   5         dgn.set_charset('gb2312')
   6         return fn(dgn,*args)
   7     return _init
   8 def _baseAuth(dgn,func,realm='Protected'):
   9     '''权限函数,只有我能上'''
  10     t=dgn.get_baseauth()
  11     if t is not  None and t==('lihui','password'):
  12         result=None
  13     else:
  14         dgn.set_status(401)
  15         dgn.set_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
  16         result=True
  17     return result
  18 #--web前端--
  19 def helloworld1(dgn):
  20     '''发布一个函数,dgn为数据api集中调用入口,为第一个参数,dgn使用的singlon模式,除系统内置定义外,可自行加入定义,比如dbpool,可定义为dgn.db.dbpool
  21 '''
  22     return 'helloworld1'
  23 
  24 def helloworld2(dgn):
  25     '''发布一个函数,加个权限'''
  26     return 'helloworld1'
  27 helloworld2._q_access=_baseAuth
  28 
  29 def helloworld3(dgn):
  30     '''发布一个函数,gb2312发布'''
  31     return 'helloworld1'
  32 helloworld3=_charsetGB2312(helloworld3)
  33 
  34 def helloworld4(dgn,*args):
  35     '''有args的支持default,比如这个函数url为http:/***/p,这个http:/***/p/1/1/1.html,则转为args=['1','1','1.html']'''
  36     return 'helloworld1'
  37 
  38 def helloworld5(dgn):
  39     '''发布一个图形文件'''
  40     return 图文件
  41 
  42 
  43 def images(dgn,*args):
  44     '''发布images子目录,静态目录'''
  45     return dgn.StaticDirectory(os.path.join(dgn.get_config().path_top,'images'),*args)
  46 
  47 #--发布函数
  48 def root():
  49     result=helloworld1
  50     result.images=images
  51     result.hl2=helloworld2
  52     result.hl3=helloworld3
  53     result.hl4=helloworld4
  54     result.hl5_jpg=helloworld5  #扩展名为jpg
  55     return result

网站架构组合

原来一直用的是zope的zpt和dtml,cherrytemplate是两者的混合,使用很方便,模块就选它拉

sqlobject真不错,实行了关系数据库+逻辑+对象处理,zodb和存储过程俺再也不用拉。

俺的最佳组合就是dragon+cherrytemplate+sqlobject

sqlobject一段代码

谈sqlobject的好象不是很多,发一段俺的代码,见识一下sqlobject的必杀绝技

from sqlobject import *
__connection__='mysql://root:123@localhost:3306/bm'

class UserMark(SQLObject):

    class sqlmeta(sqlmeta):

        table = "user_mark"
        idName = "id"
        #style = Style()
        defaultOrder = 'id'
        _cacheValues = True

    username=StringCol(alternateID=True,length=30,notNone=True,default='')
    userpasswd=StringCol(length=20,notNone=True,default='')
    userdisplay=StringCol(length=60,notNone=True,default='')
    #----------------------------------
    ip=StringCol(length=40,notNone=True,default='')
    #----------------------------------
    sex=StringCol(length=2,notNone=True,default='') #本地库表
    email=StringCol(length=60,notNone=True,default='') #本地库表
    qq=StringCol(length=20,notNone=True,default='') #本地库表
    web=StringCol(length=60,notNone=True,default='') #记录ID
    msn=StringCol(length=60,notNone=True,default='') #记录ID
    timeCreate=DateTimeCol(notNone=True,default=now)
    countAccess=IntCol(notNone=True,default=0)
    countReply=IntCol(notNone=True,default=0)
    countSubmit=IntCol(notNone=True,default=0)
    userType=IntCol(notNone=True,default=0)
    userMoney=IntCol(notNone=True,default=0)
    #----------------------------------
    def addUser(cls,user,passwd1,passwd2,email):
        errorchar=['"',"'",'=','?','.','*','&','%','<','>',' ','!']
        try:
            if passwd1<>passwd2:
                raise 'Password is not same'
            for v in errorchar:
                if v in user or v in passwd1:
                    raise 'Error username or password'
            result=cls(username=user,userpasswd=passwd1,email=email)
        except:
            result=None
        return result
    addUser=classmethod(addUser)
    #----------------------------------
    def checkUser(cls,user,passwd):
        try:
            t=cls.byUsername(user)
            if t.userpasswd==passwd:
                result=t
            else:
                result=None
        except:
            result=None
        return result
    checkUser=classmethod(checkUser)
#----------------init db--------------
DBDict={
    'UserMark':'UserMark',
    }
def proxyDB(tname):
    return globals()[tname]
def clearDB():
    for k,v in DBDict.items():
        proxyDB(v).dropTable(True)
        proxyDB(v).createTable()

if __name__=='__main__': 
    clearDB()

Karrigell 的 quixte 发布

先发一部分:

   1 import urlparse
   2 import sys
   3 import os.path
   4 import os
   5 import time
   6 from cStringIO import StringIO
   7 import posixpath
   8 import urllib
   9 import string
  10 
  11 from quixote.publish import Publisher as _Publisher
  12 import quixote.publish
  13 from quixote.http_response import HTTPResponse as _HTTPResponse
  14 from quixote.util import FileStream
  15 from quixote.config import Config
  16 from quixote.http_response import HTTPResponse
  17 from quixote.logger import DefaultLogger
  18 
  19 
  20 
  21 #------------------------------------------------------
  22 
  23 my_k_config=dict(
  24     initFile='Karrigell.ini',
  25     PORT=8080,
  26     protectedDirs=['D:\\pythonMy\\KarrigellLib\\admin'],
  27     alias={'doc':'D:\\pythonMy\\KarrigellLib/doc','base':'D:\\pythonMy\\KarrigellLib'},
  28     globalScriptsList=['D:\\pythonMy\\KarrigellLib/myScript1.py','D:\\pythonMy\\KarrigellLib/myScript.py'],
  29     globalScripts=['myScript1','myScript'],
  30     extensions_map={'.gif':'image/gif','.jpg':'image/jpeg','.py':'text/html','.nwc':'application/nwc','.html':'text/html','.htm':'text/html'},
  31     debug=1,
  32     base='',
  33     persistentSession=False,
  34     silent=False,
  35     language='',
  36     ignore=['/favicon.ico'],
  37     serverDir=r'D:\pythonMy\KarrigellLib',
  38     #rootDir=r'D:\pythonMy\KarrigellLib',
  39     extensions=['gif','html','nwc','htm','py','jpg'],
  40     gzip=False,
  41     )
  42 
  43 
  44 import  KarrigellLib
  45 m=os.path.dirname(os.path.abspath(KarrigellLib.__file__))
  46 if m not in sys.path:
  47     sys.path.append(m)
  48 
  49 msave=sys.argv
  50 sys.argv=[]
  51 import k_config
  52 sys.argv=msave
  53 
  54 for k,v in my_k_config.items():
  55     setattr(k_config,k,v)
  56 k_config.serverDir=os.path.dirname(os.path.abspath(k_config.__file__))
  57 if  not  k_config.alias.has_key('debugger'):
  58     k_config.alias['debugger']=os.path.join(k_config.serverDir,'debugger')
  59 if not  k_config.alias.has_key('demo'):
  60     k_config.alias['demo']=os.path.join(k_config.serverDir,'demo')
  61 if not  k_config.alias.has_key('fckeditor'):
  62     k_config.alias['fckeditor']=os.path.join(k_config.serverDir,'fckeditor')
  63 if not  k_config.alias.has_key('doc'):
  64     k_config.alias['doc']=os.path.join(k_config.serverDir,'doc')
  65 if not  k_config.alias.has_key('instant_site'):
  66     k_config.alias['instant_site']=os.path.join(k_config.serverDir,'instant_site')
  67 if not  k_config.alias.has_key('components'):
  68     k_config.alias['components']=os.path.join(k_config.serverDir,'components')
  69 if not  k_config.alias.has_key('listDirectory.pih'):
  70     k_config.alias['listDirectory.pih']=os.path.join(k_config.serverDir,'listDirectory.pih')
  71 if not  k_config.alias.has_key('karrigell.css'):
  72     k_config.alias['karrigell.css']=os.path.join(k_config.serverDir,'karrigell.css')
  73 if  not  k_config.alias.has_key('admin'):
  74     k_config.alias['admin']=os.path.join(k_config.serverDir,'admin')
  75 
  76 
  77 
  78 paths=[]
  79 paths.append(os.path.join(k_config.serverDir,'databases'))
  80 for v in paths:
  81     if v not in sys.path:
  82         sys.path.append(v)
  83 
  84 
  85 import SimpleHTTPServer, KarrigellRequestHandler, URLResolution,Template
  86 
  87 
  88 
  89 def translate_path(path):
  90     """Overrides SimpleHTTPServer's translate_path to handle aliases
  91     Returns the path to a file name"""
  92     path = posixpath.normpath(urllib.unquote(path))
  93     path = path[len(k_config.base):]    # remove base beginning the url
  94     words = path.split('/')
  95     words = filter(None, words)
  96     # for ks scripts, path ends at the word ending with "ks"
  97     w1,w2=[],[]
  98     for i,word in enumerate(words):
  99         w1.append(word)
 100         if word.lower().endswith(".ks"):
 101             w2=words[i+1:]
 102             break
 103     words=w1
 104     m=words[-1]
 105     if m.endswith('.html') and '_' in m:
 106         words[-1]=m.split('_',1)[0]
 107     if words and words[0] in k_config.alias.keys():
 108         path=posixpath.normpath(k_config.alias[words[0]])
 109         path=os.path.join(k_config.alias[words[0]],string.join(words[1:],"/"))
 110     else:
 111         path=os.path.join(k_config.rootDir,string.join(words,"/"))
 112     return path
 113 
 114 def list_directory(path,dirName):
 115     script = Template.getScript(os.path.join(k_config.serverDir,'listDirectory.pih'))
 116     return script.render({'path':path,'dirName':dirName}).value
 117 
 118 setattr(Template,'list_directory',list_directory)
 119 setattr(URLResolution,'translate_path',translate_path)
 120 
 121 
 122 
 123 
 124 class QuixoteHandler(KarrigellRequestHandler.KarrigellRequestHandler,SimpleHTTPServer.SimpleHTTPRequestHandler):
 125     def __init__(self, request):
 126         self.response_status=(200,'OK')
 127         self.response_headers=[]
 128         self.request = request
 129         self.wfile = StringIO()
 130         fs=request.get_fields()
 131         self.headers=dict(request.environ.items())
 132         for item in self.headers.keys():
 133             if item.startswith("HTTP_"):
 134                 hd=item[5:]
 135                 self.headers[hd.lower()]=self.headers[item]
 136         self.command = self.headers['REQUEST_METHOD']
 137         self.body = {}
 138         if self.command == 'POST':
 139             self.body=fs
 140         self.client_address=(self.headers["REMOTE_ADDR"],0)
 141         self.path=self.path_without_qs=self.headers["PATH_INFO"]
 142         if self.headers.has_key("QUERY_STRING"):
 143             self.path+="?"+self.headers["QUERY_STRING"]
 144         self.request_version=self.headers["SERVER_PROTOCOL"]
 145         self.requestline="%s %s %s" %(self.headers["SERVER_PROTOCOL"],
 146             self.headers["REQUEST_METHOD"],self.path)
 147         try:
 148             self.handle_data()
 149         finally:
 150             sys.exc_traceback = None    # Help garbage collection
 151 
 152     def send_response(self,status, reason=None):
 153         self.response_status=(status, reason)
 154     def send_header(self,key,value):
 155         self.response_headers.append((key,value))
 156     def end_headers(self):
 157         pass
 158 
 159 
 160 
 161 class Publisher(_Publisher):
 162 
 163     def __init__(self, root_directory, logger=None, session_manager=None,config=None, **kwargs):
 164         if config is None:
 165             self.config = Config(**kwargs)
 166         else:
 167             if kwargs:
 168                 raise ValueError("cannot provide both 'config' object and"
 169                                  " config arguments")
 170             self.config = config
 171         for k in my_k_config.keys():
 172             if hasattr(self.config,k.lower()):
 173                 setattr(k_config,k,getattr(self.config,k.lower()))
 174         if logger is None:
 175             self.logger = DefaultLogger(error_log=self.config.error_log,
 176                                         access_log=self.config.access_log,
 177                                         error_email=self.config.error_email)
 178         else:
 179             self.logger = logger
 180         if session_manager is not None:
 181             self.session_manager = session_manager
 182         else:
 183             from quixote.session import NullSessionManager
 184             self.session_manager = NullSessionManager()
 185 
 186         if quixote.publish._publisher is not None:
 187             raise RuntimeError, "only one instance of Publisher allowed"
 188         quixote.publish._publisher = self
 189 
 190         self.root_directory = root_directory
 191         if hasattr(root_directory,'__file__'):
 192             k_config.rootDir=os.path.dirname(os.path.abspath(root_directory.__file__))
 193         self._request = None
 194 
 195 
 196     def try_publish(self, request):
 197         self.start_request()
 198         path = request.get_environ('PATH_INFO', '')
 199         assert path[:1] == '/'
 200         # split path into components
 201         path = path[1:].split('/')
 202         t=QuixoteHandler(request)
 203         request.response.set_status(t.response_status[0])
 204         for v in t.response_headers:
 205             request.response.set_header(v[0],v[1])
 206         output = t.wfile.getvalue()
 207         # The callable ran OK, commit any changes to the session
 208         self.finish_successful_request()
 209         return output