3 # vim:set et ts=4 fdc=0 fdn=2 fdl=0:
5 # There are no blank lines between blocks beacause i use folding from:
6 # http://www.vim.org/scripts/script.php?script_id=515
13 QWeb is a python based [http://www.python.org/doc/peps/pep-0333/ WSGI]
14 compatible web framework, it provides an infratructure to quickly build web
15 applications consisting of:
17 * A lightweight request handler (QWebRequest)
18 * An xml templating engine (QWebXml and QWebHtml)
19 * A simple name based controler (qweb_control)
20 * A standalone WSGI Server (QWebWSGIServer)
21 * A cgi and fastcgi WSGI wrapper (taken from flup)
22 * A startup function that starts cgi, factgi or standalone according to the
23 evironement (qweb_autorun).
25 QWeb applications are runnable in standalone mode (from commandline), via
26 FastCGI, Regular CGI or by any python WSGI compliant server.
28 QWeb doesn't provide any database access but it integrates nicely with ORMs
29 such as SQLObject, SQLAlchemy or plain DB-API.
31 Written by Antony Lesuisse (email al AT udev.org)
33 Homepage: http://antony.lesuisse.org/qweb/trac/
35 Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
37 == Quick Start (for Linux, MacOS X and cygwin) ==
39 Make sure you have at least python 2.3 installed and run the following commands:
42 $ wget http://antony.lesuisse.org/qweb/files/QWeb-0.7.tar.gz
43 $ tar zxvf QWeb-0.7.tar.gz
44 $ cd QWeb-0.7/examples/blog
48 And point your browser to http://localhost:8080/
50 You may also try AjaxTerm which uses qweb request handler.
55 * Source [/qweb/files/QWeb-0.7.tar.gz QWeb-0.7.tar.gz]
56 * Python 2.3 Egg [/qweb/files/QWeb-0.7-py2.3.egg QWeb-0.7-py2.3.egg]
57 * Python 2.4 Egg [/qweb/files/QWeb-0.7-py2.4.egg QWeb-0.7-py2.4.egg]
59 * [/qweb/trac/browser Browse the source repository]
63 * [/qweb/trac/browser/trunk/README.txt?format=raw Read the included documentation]
68 * Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
69 * No mailing-list exists yet, discussion should happen on: [http://mail.python.org/mailman/listinfo/web-sig web-sig] [http://mail.python.org/pipermail/web-sig/ archives]
74 QWeb also feature a simple components api, that enables developers to easily
75 produces reusable components.
77 Default qweb components:
80 A qweb component to serve static content from the filesystem or from
84 scaffolding for sqlobject
88 qweb/fcgi.py wich is BSD-like from saddi.com.
89 Everything else is put in the public domain.
94 Announce QWeb to python-announce-list@python.org web-sig@python.org
96 rename request methods into
101 request callback_generator, callback_function ?
102 wsgi callback_server_local
103 xml tags explicitly call render_attributes(t_att)?
104 priority form-checkbox over t-value (for t-option)
108 import BaseHTTPServer,SocketServer,Cookie
109 import cgi,datetime,email,email.Message,errno,gzip,os,random,re,socket,sys,tempfile,time,types,urllib,urlparse,xml.dom
111 import cPickle as pickle
115 import cStringIO as StringIO
119 #----------------------------------------------------------
120 # Qweb Xml t-raw t-esc t-if t-foreach t-set t-call t-trim
121 #----------------------------------------------------------
123 def __init__(self,data):
125 def __getitem__(self,expr):
126 if self.data.has_key(expr):
127 return self.data[expr]
130 r=eval(expr,self.data)
133 except AttributeError,e:
136 print "qweb: expression error '%s' "%expr,e
137 if self.data.has_key("__builtins__"):
138 del self.data["__builtins__"]
140 def eval_object(self,expr):
142 def eval_str(self,expr):
145 if isinstance(self[expr],unicode):
146 return self[expr].encode("utf8")
147 return str(self[expr])
148 def eval_format(self,expr):
150 return str(expr%self)
152 return "qweb: format error '%s' "%expr
153 # if isinstance(r,unicode):
154 # return r.encode("utf8")
155 def eval_bool(self,expr):
156 if self.eval_object(expr):
161 """QWeb Xml templating engine
163 The templating engine use a very simple syntax, "magic" xml attributes, to
164 produce any kind of texutal output (even non-xml).
167 the template engine core implements the basic magic attributes:
169 t-att t-raw t-esc t-if t-foreach t-set t-call t-trim
172 def __init__(self,x=None,zipname=None):
173 self.node=xml.dom.Node
177 for i in [j for j in dir(self) if j.startswith(prefix)]:
178 name=i[len(prefix):].replace('_','-')
179 self._render_tag[name]=getattr(self.__class__,i)
183 for i in [j for j in dir(self) if j.startswith(prefix)]:
184 name=i[len(prefix):].replace('_','-')
185 self._render_att[name]=getattr(self.__class__,i)
190 zf=zipfile.ZipFile(zipname, 'r')
191 self.add_template(zf.read(x))
194 def register_tag(self,tag,func):
195 self._render_tag[tag]=func
196 def add_template(self,x):
197 if hasattr(x,'documentElement'):
199 elif x.startswith("<?xml"):
200 import xml.dom.minidom
201 dom=xml.dom.minidom.parseString(x)
203 import xml.dom.minidom
204 dom=xml.dom.minidom.parse(x)
205 for n in dom.documentElement.childNodes:
207 self._t[str(n.getAttribute("t-name"))]=n
208 def get_template(self,name):
211 def eval_object(self,expr,v):
212 return QWebEval(v).eval_object(expr)
213 def eval_str(self,expr,v):
214 return QWebEval(v).eval_str(expr)
215 def eval_format(self,expr,v):
216 return QWebEval(v).eval_format(expr)
217 def eval_bool(self,expr,v):
218 return QWebEval(v).eval_bool(expr)
220 def render(self,tname,v={},out=None):
221 if self._t.has_key(tname):
222 return self.render_node(self._t[tname],v)
224 return 'qweb: template "%s" not found'%tname
225 def render_node(self,e,v):
227 if e.nodeType==self.node.TEXT_NODE or e.nodeType==self.node.CDATA_SECTION_NODE:
228 r=e.data.encode("utf8")
229 elif e.nodeType==self.node.ELEMENT_NODE:
234 for (an,av) in e.attributes.items():
236 if isinstance(av,types.UnicodeType):
239 av=av.nodeValue.encode("utf8")
240 if an.startswith("t-"):
241 for i in self._render_att:
242 if an[2:].startswith(i):
243 g_att+=self._render_att[i](self,e,an,av,v)
246 if self._render_tag.has_key(an[2:]):
250 g_att+=' %s="%s"'%(an,cgi.escape(av,1));
252 if self._render_tag.has_key(t_render):
253 r=self._render_tag[t_render](self,e,t_att,g_att,v)
255 r=self.render_element(e,g_att,v,pre,t_att.get("trim",0))
257 def render_element(self,e,g_att,v,pre="",trim=0):
259 for n in e.childNodes:
260 g_inner.append(self.render_node(n,v))
262 inner="".join(g_inner)
274 return "<%s%s>%s%s</%s>"%(name,g_att,pre,inner,name)
276 return "<%s%s/>"%(name,g_att)
279 def render_att_att(self,e,an,av,v):
280 if an.startswith("t-attf-"):
281 att,val=an[7:],self.eval_format(av,v)
282 elif an.startswith("t-att-"):
283 att,val=(an[6:],self.eval_str(av,v))
285 att,val=self.eval_object(av,v)
286 return ' %s="%s"'%(att,cgi.escape(val,1))
289 def render_tag_raw(self,e,t_att,g_att,v):
290 return self.eval_str(t_att["raw"],v)
291 def render_tag_rawf(self,e,t_att,g_att,v):
292 return self.eval_format(t_att["rawf"],v)
293 def render_tag_esc(self,e,t_att,g_att,v):
294 return cgi.escape(self.eval_str(t_att["esc"],v))
295 def render_tag_escf(self,e,t_att,g_att,v):
296 return cgi.escape(self.eval_format(t_att["escf"],v))
297 def render_tag_foreach(self,e,t_att,g_att,v):
298 expr=t_att["foreach"]
299 enum=self.eval_object(expr,v)
301 var=t_att.get('as',expr).replace('.','_')
304 if isinstance(enum,types.ListType):
306 elif isinstance(enum,types.TupleType):
308 elif hasattr(enum,'count'):
310 d["%s_size"%var]=size
316 d["%s_index"%var]=index
317 d["%s_first"%var]=index==0
318 d["%s_even"%var]=index%2
319 d["%s_odd"%var]=(index+1)%2
320 d["%s_last"%var]=index+1==size
322 d["%s_parity"%var]='odd'
324 d["%s_parity"%var]='even'
325 if isinstance(i,types.DictType):
329 ru.append(self.render_element(e,g_att,d))
333 return "qweb: t-foreach %s not found."%expr
334 def render_tag_if(self,e,t_att,g_att,v):
335 if self.eval_bool(t_att["if"],v):
336 return self.render_element(e,g_att,v)
339 def render_tag_call(self,e,t_att,g_att,v):
341 if t_att.has_key("import"):
345 d[0]=self.render_element(e,g_att,d)
346 return self.render(t_att["call"],d)
347 def render_tag_set(self,e,t_att,g_att,v):
348 if t_att.has_key("eval"):
349 v[t_att["set"]]=self.eval_object(t_att["eval"],v)
351 v[t_att["set"]]=self.render_element(e,g_att,v)
354 #----------------------------------------------------------
355 # QWeb HTML (+deprecated QWebFORM and QWebOLD)
356 #----------------------------------------------------------
359 assert req.PATH_INFO== "/site/admin/page_edit"
360 u = QWebURL(root_path="/site/",req_path=req.PATH_INFO)
361 s=u.url2_href("user/login",{'a':'1'})
362 assert s=="../user/login?a=1"
365 def __init__(self, root_path="/", req_path="/",defpath="",defparam={}):
367 self.defparam=defparam
368 self.root_path=root_path
369 self.req_path=req_path
370 self.req_list=req_path.split("/")[:-1]
371 self.req_len=len(self.req_list)
374 for k,v in cgi.parse_qsl(s,1):
378 return urllib.urlencode(h.items())
379 def request(self,req):
381 def copy(self,path=None,param=None):
385 nparam=self.defparam.copy()
388 return QWebURL(self.root_path,self.req_path,npath,nparam)
389 def path(self,path=''):
392 pl=(self.root_path+path).split('/')
394 for i in range(min(len(pl), self.req_len)):
395 if pl[i]!=self.req_list[i]:
402 return '/'.join(['..']*dd+pl[i:])
403 def href(self,path='',arg={}):
405 tmp=self.defparam.copy()
412 def form(self,path='',arg={}):
414 tmp=self.defparam.copy()
416 r=''.join(['<input type="hidden" name="%s" value="%s"/>'%(k,cgi.escape(str(v),1)) for k,v in tmp.items()])
419 def __init__(self,name=None,default="",check=None):
423 # optional attributes
427 self.cssvalid="form_valid"
428 self.cssinvalid="form_invalid"
438 def validate(self,val=1,update=1):
442 self.css=self.cssvalid
446 self.css=self.cssinvalid
447 if update and self.form:
449 def invalidate(self,update=1):
450 self.validate(0,update)
454 def __init__(self,e=None,arg=None,default=None):
456 # all fields have been submitted
459 # at least one field is invalid or missing
462 # all fields have been submitted and are valid
464 # fields under self.f for convenience
465 self.f=self.QWebFormF()
468 # assume that the fields are done with the template
470 self.set_default(default,e==None)
472 self.process_input(arg)
473 def __getitem__(self,k):
474 return self.fields[k]
475 def set_default(self,default,add_missing=1):
476 for k,v in default.items():
477 if self.fields.has_key(k):
478 self.fields[k].default=str(v)
480 self.add_field(QWebField(k,v))
481 def add_field(self,f):
482 self.fields[f.name]=f
484 setattr(self.f,f.name,f)
485 def add_template(self,e):
487 for (an,av) in e.attributes.items():
489 if an.startswith("t-"):
490 att[an[2:]]=av.encode("utf8")
491 for i in ["form-text", "form-password", "form-radio", "form-checkbox", "form-select","form-textarea"]:
493 name=att[i].split(".")[-1]
494 default=att.get("default","")
495 check=att.get("check",None)
496 f=QWebField(name,default,check)
497 if i=="form-textarea":
500 if i=="form-checkbox":
504 for n in e.childNodes:
505 if n.nodeType==n.ELEMENT_NODE:
507 def process_input(self,arg):
508 for f in self.fields.values():
509 if arg.has_key(f.name):
513 f.input=f.input.strip()
517 elif callable(f.check):
519 elif isinstance(f.check,str):
522 v=r"/^[^@#!& ]+@[A-Za-z0-9-][.A-Za-z0-9-]{0,64}\.[A-Za-z]{2,5}$/"
524 v=r"/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/"
525 if not re.match(v[1:-1],f.input):
530 def validate_all(self,val=1):
531 for f in self.fields.values():
534 def invalidate_all(self):
540 for f in self.fields.values():
541 if f.required and f.input==None:
544 self.missing.append(f.name)
547 self.error.append(f.name)
548 # invalid have been submitted and
549 self.invalid=self.submitted and self.valid==False
552 for f in self.fields.values():
555 class QWebURLEval(QWebEval):
556 def __init__(self,data):
557 QWebEval.__init__(self,data)
558 def __getitem__(self,expr):
559 r=QWebEval.__getitem__(self,expr)
560 if isinstance(r,str):
561 return urllib.quote_plus(r)
564 class QWebHtml(QWebXml):
570 an extended template engine, with a few utility class to easily produce
571 HTML, handle URLs and process forms, it adds the following magic attributes:
573 t-href t-action t-form-text t-form-password t-form-textarea t-form-radio
574 t-form-checkbox t-form-select t-option t-selected t-checked t-pager
577 # v['tableurl']=QWebUrl({p=afdmin,saar=,orderby=,des=,mlink;meta_active=})
578 # t-href="tableurl?desc=1"
580 # explication FORM: t-if="form.valid()"
582 # email: <input type="text" t-esc-name="i" t-esc-value="form[i].value" t-esc-class="form[i].css"/>
583 # <input type="radio" name="spamtype" t-esc-value="i" t-selected="i==form.f.spamtype.value"/>
584 # <option t-esc-value="cc" t-selected="cc==form.f.country.value"><t t-esc="cname"></option>
586 # <input t-form-text="form.email" t-check="email"/>
587 # <input t-form-password="form.email" t-check="email"/>
588 # <input t-form-radio="form.email" />
589 # <input t-form-checkbox="form.email" />
590 # <textarea t-form-textarea="form.email" t-check="email"/>
591 # <select t-form-select="form.email"/>
592 # <option t-value="1">
593 # <input t-form-radio="form.spamtype" t-value="1"/> Cars
594 # <input t-form-radio="form.spamtype" t-value="2"/> Sprt
596 # QWebForm from a template
597 def form(self,tname,arg=None,default=None):
598 form=QWebForm(self._t[tname],arg,default)
602 def eval_url(self,av,v):
603 s=QWebURLEval(v).eval_format(av)
607 for k,v in cgi.parse_qsl(a[1],1):
615 def render_att_url_(self,e,an,av,v):
616 u,path,arg=self.eval_url(av,v)
617 if not isinstance(v.get(u,0),QWebURL):
618 out='qweb: missing url %r %r %r'%(u,path,arg)
620 out=v[u].href(path,arg)
621 return ' %s="%s"'%(an[6:],cgi.escape(out,1))
622 def render_att_href(self,e,an,av,v):
623 return self.render_att_url_(e,"t-url-href",av,v)
624 def render_att_checked(self,e,an,av,v):
625 if self.eval_bool(av,v):
626 return ' %s="%s"'%(an[2:],an[2:])
629 def render_att_selected(self,e,an,av,v):
630 return self.render_att_checked(e,an,av,v)
633 def render_tag_rawurl(self,e,t_att,g_att,v):
634 u,path,arg=self.eval_url(t_att["rawurl"],v)
635 return v[u].href(path,arg)
636 def render_tag_escurl(self,e,t_att,g_att,v):
637 u,path,arg=self.eval_url(t_att["escurl"],v)
638 return cgi.escape(v[u].href(path,arg))
639 def render_tag_action(self,e,t_att,g_att,v):
640 u,path,arg=self.eval_url(t_att["action"],v)
641 if not isinstance(v.get(u,0),QWebURL):
642 action,input=('qweb: missing url %r %r %r'%(u,path,arg),'')
644 action,input=v[u].form(path,arg)
645 g_att+=' action="%s"'%action
646 return self.render_element(e,g_att,v,input)
647 def render_tag_form_text(self,e,t_att,g_att,v):
648 f=self.eval_object(t_att["form-text"],v)
649 g_att+=' type="text" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
650 return self.render_element(e,g_att,v)
651 def render_tag_form_password(self,e,t_att,g_att,v):
652 f=self.eval_object(t_att["form-password"],v)
653 g_att+=' type="password" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
654 return self.render_element(e,g_att,v)
655 def render_tag_form_textarea(self,e,t_att,g_att,v):
657 f=self.eval_object(t_att["form-textarea"],v)
658 g_att+=' name="%s" class="%s"'%(f.name,f.css)
659 r="<%s%s>%s</%s>"%(type,g_att,cgi.escape(f.value,1),type)
661 def render_tag_form_radio(self,e,t_att,g_att,v):
662 f=self.eval_object(t_att["form-radio"],v)
664 g_att+=' type="radio" name="%s" value="%s"'%(f.name,val)
666 g_att+=' checked="checked"'
667 return self.render_element(e,g_att,v)
668 def render_tag_form_checkbox(self,e,t_att,g_att,v):
669 f=self.eval_object(t_att["form-checkbox"],v)
671 g_att+=' type="checkbox" name="%s" value="%s"'%(f.name,val)
673 g_att+=' checked="checked"'
674 return self.render_element(e,g_att,v)
675 def render_tag_form_select(self,e,t_att,g_att,v):
676 f=self.eval_object(t_att["form-select"],v)
677 g_att+=' name="%s" class="%s"'%(f.name,f.css)
678 return self.render_element(e,g_att,v)
679 def render_tag_option(self,e,t_att,g_att,v):
680 f=self.eval_object(e.parentNode.getAttribute("t-form-select"),v)
682 g_att+=' value="%s"'%(val)
684 g_att+=' selected="selected"'
685 return self.render_element(e,g_att,v)
688 def render_tag_pager(self,e,t_att,g_att,v):
690 total=int(self.eval_str(t_att["total"],v))
691 start=int(self.eval_str(t_att["start"],v))
692 step=int(self.eval_str(t_att.get("step","100"),v))
693 scope=int(self.eval_str(t_att.get("scope","5"),v))
697 d[p+"tot_size"]=total
698 d[p+"tot_page"]=tot_page=total/step
699 d[p+"win_start0"]=total and start
700 d[p+"win_start1"]=total and start+1
701 d[p+"win_end0"]=max(0,min(start+step-1,total-1))
702 d[p+"win_end1"]=min(start+step,total)
703 d[p+"win_page0"]=win_page=start/step
704 d[p+"win_page1"]=win_page+1
705 d[p+"prev"]=(win_page!=0)
706 d[p+"prev_start"]=(win_page-1)*step
707 d[p+"next"]=(tot_page>=win_page+1)
708 d[p+"next_start"]=(win_page+1)*step
715 begin-=(end-tot_page)
717 while i<=min(end,tot_page) and total!=step:
718 l.append( { p+"page0":i, p+"page1":i+1, p+"start":i*step, p+"sel":(win_page==i) })
720 d[p+"active"]=len(l)>1
726 #----------------------------------------------------------
727 # QWeb Simple Controller
728 #----------------------------------------------------------
729 def qweb_control(self,jump='main',p=[]):
730 """ qweb_control(self,jump='main',p=[]):
731 A simple function to handle the controler part of your application. It
732 dispatch the control to the jump argument, while ensuring that prefix
733 function have been called.
735 qweb_control replace '/' to '_' and strip '_' from the jump argument.
742 jump=jump.replace('/','_').strip('_')
743 if not hasattr(self,jump):
751 for i in jump.split("_"):
753 if not done.has_key(tmp[:-1]):
754 todo.append(tmp[:-1])
762 if isinstance(r,types.StringType):
768 #----------------------------------------------------------
769 # QWeb WSGI Request handler
770 #----------------------------------------------------------
771 class QWebSession(dict):
772 def __init__(self,environ,**kw):
775 "path" : tempfile.gettempdir(),
776 "cookie_name" : "QWEBSID",
777 "cookie_lifetime" : 0,
779 "cookie_domain" : '',
781 "probability" : 0.01,
782 "maxlifetime" : 3600,
785 for k,v in default.items():
786 setattr(self,'session_%s'%k,kw.get(k,v))
787 # Try to find session
788 self.session_found_cookie=0
789 self.session_found_url=0
793 c=Cookie.SimpleCookie()
794 c.load(environ.get('HTTP_COOKIE', ''))
795 if c.has_key(self.session_cookie_name):
796 sid=c[self.session_cookie_name].value[:64]
797 if re.match('[a-f0-9]+$',sid) and self.session_load(sid):
799 self.session_found_cookie=1
802 if not self.session_found_cookie:
803 mo=re.search('&%s=([a-f0-9]+)'%self.session_cookie_name,environ.get('QUERY_STRING',''))
804 if mo and self.session_load(mo.group(1)):
805 self.session_id=mo.group(1)
806 self.session_found_url=1
809 if not self.session_found:
810 self.session_id='%032x'%random.randint(1,2**128)
811 self.session_trans_sid="&%s=%s"%(self.session_cookie_name,self.session_id)
813 if random.random() < self.session_probability:
815 def session_get_headers(self):
817 if (not self.session_disable) and (len(self) or len(self.session_orig)):
819 if not self.session_found_cookie:
820 c=Cookie.SimpleCookie()
821 c[self.session_cookie_name] = self.session_id
822 c[self.session_cookie_name]['path'] = self.session_cookie_path
823 if self.session_cookie_domain:
824 c[self.session_cookie_name]['domain'] = self.session_cookie_domain
825 # if self.session_cookie_lifetime:
826 # c[self.session_cookie_name]['expires'] = TODO date localtime or not, datetime.datetime(1970, 1, 1)
827 h.append(("Set-Cookie", c[self.session_cookie_name].OutputString()))
828 if self.session_limit_cache:
829 h.append(('Cache-Control','no-store, no-cache, must-revalidate, post-check=0, pre-check=0'))
830 h.append(('Expires','Thu, 19 Nov 1981 08:52:00 GMT'))
831 h.append(('Pragma','no-cache'))
833 def session_load(self,sid):
834 fname=os.path.join(self.session_path,'qweb_sess_%s'%sid)
836 orig=file(fname).read()
840 self.session_orig=orig
843 def session_save(self):
844 if not os.path.isdir(self.session_path):
845 os.makedirs(self.session_path)
846 fname=os.path.join(self.session_path,'qweb_sess_%s'%self.session_id)
848 oldtime=os.path.getmtime(fname)
849 except OSError,IOError:
851 dump=pickle.dumps(self.copy())
852 if (dump != self.session_orig) or (time.time() > oldtime+self.session_maxlifetime/4):
853 tmpname=os.path.join(self.session_path,'qweb_sess_%s_%x'%(self.session_id,random.randint(1,2**32)))
857 if sys.platform=='win32' and os.path.isfile(fname):
859 os.rename(tmpname,fname)
860 def session_clean(self):
863 for i in [os.path.join(self.session_path,i) for i in os.listdir(self.session_path) if i.startswith('qweb_sess_')]:
864 if (t > os.path.getmtime(i)+self.session_maxlifetime):
866 except OSError,IOError:
868 class QWebSessionMem(QWebSession):
869 def session_load(self,sid):
870 global _qweb_sessions
871 if not "_qweb_sessions" in globals():
873 if _qweb_sessions.has_key(sid):
874 self.session_orig=_qweb_sessions[sid]
875 self.update(self.session_orig)
877 def session_save(self):
878 global _qweb_sessions
879 if not "_qweb_sessions" in globals():
881 _qweb_sessions[self.session_id]=self.copy()
882 class QWebSessionService:
883 def __init__(self, wsgiapp, url_rewrite=0):
885 self.url_rewrite_tags="a=href,area=href,frame=src,form=,fieldset="
886 def __call__(self, environ, start_response):
888 # use QWebSession to provide environ["qweb.session"]
889 return self.wsgiapp(environ,start_response)
890 class QWebDict(dict):
891 def __init__(self,*p):
892 dict.__init__(self,*p)
893 def __getitem__(self,key):
894 return self.get(key,"")
897 return int(self.get(key,"0"))
900 class QWebListDict(dict):
901 def __init__(self,*p):
902 dict.__init__(self,*p)
903 def __getitem__(self,key):
904 return self.get(key,[])
905 def appendlist(self,key,val):
906 if self.has_key(key):
907 self[key].append(val)
910 def get_qwebdict(self):
912 for k,v in self.items():
916 """QWebRequest a WSGI request handler.
918 QWebRequest is a WSGI request handler that feature GET, POST and POST
919 multipart methods, handles cookies and headers and provide a dict-like
920 SESSION Object (either on the filesystem or in memory).
922 It is constructed with the environ and start_response WSGI arguments:
924 req=qweb.QWebRequest(environ, start_response)
926 req has the folowing attributes :
928 req.environ standard WSGI dict (CGI and wsgi ones)
930 Some CGI vars as attributes from environ for convenience:
936 Some computed value (also for convenience)
938 req.FULL_URL full URL recontructed (http://host/query)
939 req.FULL_PATH (URL path before ?querystring)
941 Dict constructed from querystring and POST datas, PHP-like.
943 req.GET contains GET vars
944 req.POST contains POST vars
945 req.REQUEST contains merge of GET and POST
946 req.FILES contains uploaded files
947 req.GET_LIST req.POST_LIST req.REQUEST_LIST req.FILES_LIST multiple arguments versions
948 req.debug() returns an HTML dump of those vars
950 A dict-like session object.
952 req.SESSION the session start when the dict is not empty.
954 Attribute for handling the response
956 req.response_headers dict-like to set headers
957 req.response_cookies a SimpleCookie to set cookies
958 req.response_status a string to set the status like '200 OK'
960 req.write() to write to the buffer
962 req itselfs is an iterable object with the buffer, it will also also call
963 start_response automatically before returning anything via the iterator.
965 To make it short, it means that you may use
969 at the end of your request handling to return the reponse to any WSGI
973 # This class contains part ripped from colubrid (with the permission of
974 # mitsuhiko) see http://wsgiarea.pocoo.org/colubrid/
976 # - the class HttpHeaders
977 # - the method load_post_data (tuned version)
979 class HttpHeaders(object):
981 self.data = [('Content-Type', 'text/html')]
982 def __setitem__(self, key, value):
984 def __delitem__(self, key):
986 def __contains__(self, key):
988 for k, v in self.data:
992 def add(self, key, value):
993 self.data.append((key, value))
994 def remove(self, key, count=-1):
997 for _key, _value in self.data:
998 if _key.lower() != key.lower():
1000 if removed >= count:
1004 data.append((_key, _value))
1008 def set(self, key, value):
1010 self.add(key, value)
1011 def get(self, key=False, httpformat=False):
1016 for _key, _value in self.data:
1017 if _key.lower() == key.lower():
1018 result.append((_key, _value))
1020 return '\n'.join(['%s: %s' % item for item in result])
1022 def load_post_data(self,environ,POST,FILES):
1023 length = int(environ['CONTENT_LENGTH'])
1024 DATA = environ['wsgi.input'].read(length)
1025 if environ.get('CONTENT_TYPE', '').startswith('multipart'):
1026 lines = ['Content-Type: %s' % environ.get('CONTENT_TYPE', '')]
1027 for key, value in environ.items():
1028 if key.startswith('HTTP_'):
1029 lines.append('%s: %s' % (key, value))
1030 raw = '\r\n'.join(lines) + '\r\n\r\n' + DATA
1031 msg = email.message_from_string(raw)
1032 for sub in msg.get_payload():
1033 if not isinstance(sub, email.Message.Message):
1035 name_dict = cgi.parse_header(sub['Content-Disposition'])[1]
1036 if 'filename' in name_dict:
1037 # Nested MIME Messages are not supported'
1038 if type([]) == type(sub.get_payload()):
1040 if not name_dict['filename'].strip():
1042 filename = name_dict['filename']
1043 # why not keep all the filename? because IE always send 'C:\documents and settings\blub\blub.png'
1044 filename = filename[filename.rfind('\\') + 1:]
1045 if 'Content-Type' in sub:
1046 content_type = sub['Content-Type']
1049 s = { "name":filename, "type":content_type, "data":sub.get_payload() }
1050 FILES.appendlist(name_dict['name'], s)
1052 POST.appendlist(name_dict['name'], sub.get_payload())
1054 POST.update(cgi.parse_qs(DATA,keep_blank_values=1))
1057 def __init__(self,environ,start_response,session=QWebSession):
1058 self.environ=environ
1059 self.start_response=start_response
1062 self.SCRIPT_NAME = environ.get('SCRIPT_NAME', '')
1063 self.PATH_INFO = environ.get('PATH_INFO', '')
1065 self.FULL_URL = environ['FULL_URL'] = self.get_full_url(environ)
1066 # REQUEST_URI is optional, fake it if absent
1067 if not environ.has_key("REQUEST_URI"):
1068 environ["REQUEST_URI"]=urllib.quote(self.SCRIPT_NAME+self.PATH_INFO)
1069 if environ.get('QUERY_STRING'):
1070 environ["REQUEST_URI"]+='?'+environ['QUERY_STRING']
1071 self.REQUEST_URI = environ["REQUEST_URI"]
1072 # full quote url path before the ?
1073 self.FULL_PATH = environ['FULL_PATH'] = self.REQUEST_URI.split('?')[0]
1075 self.request_cookies=Cookie.SimpleCookie()
1076 self.request_cookies.load(environ.get('HTTP_COOKIE', ''))
1078 self.response_started=False
1079 self.response_gzencode=False
1080 self.response_cookies=Cookie.SimpleCookie()
1081 # to delete a cookie use: c[key]['expires'] = datetime.datetime(1970, 1, 1)
1082 self.response_headers=self.HttpHeaders()
1083 self.response_status="200 OK"
1086 if self.environ.has_key("php"):
1087 self.php=environ["php"]
1088 self.SESSION=self.php._SESSION
1089 self.GET=self.php._GET
1090 self.POST=self.php._POST
1091 self.REQUEST=self.php._ARG
1092 self.FILES=self.php._FILES
1094 if isinstance(session,QWebSession):
1095 self.SESSION=session
1097 self.SESSION=session(environ)
1100 self.GET_LIST=QWebListDict(cgi.parse_qs(environ.get('QUERY_STRING', ''),keep_blank_values=1))
1101 self.POST_LIST=QWebListDict()
1102 self.FILES_LIST=QWebListDict()
1103 self.REQUEST_LIST=QWebListDict(self.GET_LIST)
1104 if environ['REQUEST_METHOD'] == 'POST':
1105 self.DATA=self.load_post_data(environ,self.POST_LIST,self.FILES_LIST)
1106 self.REQUEST_LIST.update(self.POST_LIST)
1107 self.GET=self.GET_LIST.get_qwebdict()
1108 self.POST=self.POST_LIST.get_qwebdict()
1109 self.FILES=self.FILES_LIST.get_qwebdict()
1110 self.REQUEST=self.REQUEST_LIST.get_qwebdict()
1111 def get_full_url(environ):
1112 # taken from PEP 333
1113 if 'FULL_URL' in environ:
1114 return environ['FULL_URL']
1115 url = environ['wsgi.url_scheme']+'://'
1116 if environ.get('HTTP_HOST'):
1117 url += environ['HTTP_HOST']
1119 url += environ['SERVER_NAME']
1120 if environ['wsgi.url_scheme'] == 'https':
1121 if environ['SERVER_PORT'] != '443':
1122 url += ':' + environ['SERVER_PORT']
1124 if environ['SERVER_PORT'] != '80':
1125 url += ':' + environ['SERVER_PORT']
1126 if environ.has_key('REQUEST_URI'):
1127 url += environ['REQUEST_URI']
1129 url += urllib.quote(environ.get('SCRIPT_NAME', ''))
1130 url += urllib.quote(environ.get('PATH_INFO', ''))
1131 if environ.get('QUERY_STRING'):
1132 url += '?' + environ['QUERY_STRING']
1134 get_full_url=staticmethod(get_full_url)
1135 def save_files(self):
1136 for k,v in self.FILES.items():
1137 if not v.has_key("tmp_file"):
1138 f=tempfile.NamedTemporaryFile()
1142 v["tmp_name"]=f.name
1146 ("GET",self.GET), ("POST",self.POST), ("REQUEST",self.REQUEST), ("FILES",self.FILES),
1147 ("GET_LIST",self.GET_LIST), ("POST_LIST",self.POST_LIST), ("REQUEST_LIST",self.REQUEST_LIST), ("FILES_LIST",self.FILES_LIST),
1148 ("SESSION",self.SESSION), ("environ",self.environ),
1150 body+='<table border="1" width="100%" align="center">\n'
1151 body+='<tr><th colspan="2" align="center">%s</th></tr>\n'%name
1154 body+=''.join(['<tr><td>%s</td><td>%s</td></tr>\n'%(k,cgi.escape(repr(d[k]))) for k in keys])
1155 body+='</table><br><br>\n\n'
1158 self.buffer.append(s)
1160 self.buffer.extend([str(i) for i in s])
1162 if not self.response_started:
1164 for k,v in self.FILES.items():
1165 if v.has_key("tmp_file"):
1167 v["tmp_file"].close()
1170 if self.response_gzencode and self.environ.get('HTTP_ACCEPT_ENCODING','').find('gzip')!=-1:
1171 zbuf=StringIO.StringIO()
1172 zfile=gzip.GzipFile(mode='wb', fileobj=zbuf)
1173 zfile.write(''.join(self.buffer))
1175 zbuf=zbuf.getvalue()
1177 self.response_headers['Content-Encoding']="gzip"
1178 self.response_headers['Content-Length']=str(len(zbuf))
1179 headers = self.response_headers.get()
1180 if isinstance(self.SESSION, QWebSession):
1181 headers.extend(self.SESSION.session_get_headers())
1182 headers.extend([('Set-Cookie', self.response_cookies[i].OutputString()) for i in self.response_cookies])
1183 self.start_response(self.response_status, headers)
1184 self.response_started=True
1187 return self.response().__iter__()
1188 def http_redirect(self,url,permanent=1):
1190 self.response_status="301 Moved Permanently"
1192 self.response_status="302 Found"
1193 self.response_headers["Location"]=url
1194 def http_404(self,msg="<h1>404 Not Found</h1>"):
1195 self.response_status="404 Not Found"
1198 def http_download(self,fname,fstr,partial=0):
1199 # allow fstr to be a file-like object
1201 # say accept ranages
1202 # parse range headers...
1204 # header("HTTP/1.1 206 Partial Content");
1205 # header("Content-Range: bytes $offset-".($fsize-1)."/".$fsize);
1206 # header("Content-Length: ".($fsize-$offset));
1207 # fseek($fd,$offset);
1209 self.response_headers["Content-Type"]="application/octet-stream"
1210 self.response_headers["Content-Disposition"]="attachment; filename=\"%s\""%fname
1211 self.response_headers["Content-Transfer-Encoding"]="binary"
1212 self.response_headers["Content-Length"]="%d"%len(fstr)
1215 #----------------------------------------------------------
1216 # QWeb WSGI HTTP Server to run any WSGI app
1217 # autorun, run an app as FCGI or CGI otherwise launch the server
1218 #----------------------------------------------------------
1219 class QWebWSGIHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1220 def log_message(self,*p):
1222 return BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,*p)
1223 def address_string(self):
1224 return self.client_address[0]
1225 def start_response(self,status,headers):
1226 l=status.split(' ',1)
1227 self.send_response(int(l[0]),l[1])
1230 if i[0].lower()=="content-type":
1232 self.send_header(*i)
1234 self.send_header("Content-type", "text/html")
1237 def write(self,data):
1239 self.wfile.write(data)
1240 except (socket.error, socket.timeout),e:
1243 if not getattr(self,'wfile_buf',0):
1245 self.wfile_bak=self.wfile
1246 self.wfile=StringIO.StringIO()
1247 def bufferoff(self):
1250 self.wfile=self.wfile_bak
1251 self.write(buf.getvalue())
1253 def serve(self,type):
1254 path_info, parameters, query = urlparse.urlparse(self.path)[2:5]
1256 'wsgi.version': (1,0),
1257 'wsgi.url_scheme': 'http',
1258 'wsgi.input': self.rfile,
1259 'wsgi.errors': sys.stderr,
1260 'wsgi.multithread': 0,
1261 'wsgi.multiprocess': 0,
1263 'REQUEST_METHOD': self.command,
1265 'QUERY_STRING': query,
1266 'CONTENT_TYPE': self.headers.get('Content-Type', ''),
1267 'CONTENT_LENGTH': self.headers.get('Content-Length', ''),
1268 'REMOTE_ADDR': self.client_address[0],
1269 'REMOTE_PORT': str(self.client_address[1]),
1270 'SERVER_NAME': self.server.server_address[0],
1271 'SERVER_PORT': str(self.server.server_address[1]),
1272 'SERVER_PROTOCOL': self.request_version,
1274 'FULL_PATH': self.path,
1275 'qweb.mode': 'standalone',
1278 environ['PATH_INFO'] = urllib.unquote(path_info)
1279 for key, value in self.headers.items():
1280 environ['HTTP_' + key.upper().replace('-', '_')] = value
1281 # Hack to avoid may TCP packets
1283 appiter=self.server.wsgiapp(environ, self.start_response)
1284 for data in appiter:
1292 class QWebWSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
1294 qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1)
1295 A WSGI HTTP server threaded or not and a function to automatically run your
1296 app according to the environement (either standalone, CGI or FastCGI).
1298 This feature is called QWeb autorun. If you want to To use it on your
1299 application use the following lines at the end of the main application
1302 if __name__ == '__main__':
1303 qweb.qweb_wsgi_autorun(your_wsgi_app)
1305 this function will select the approriate running mode according to the
1306 calling environement (http-server, FastCGI or CGI).
1308 def __init__(self, wsgiapp, ip, port, threaded=1, log=1):
1309 BaseHTTPServer.HTTPServer.__init__(self, (ip, port), QWebWSGIHandler)
1310 self.wsgiapp = wsgiapp
1311 self.threaded = threaded
1313 def process_request(self,*p):
1315 return SocketServer.ThreadingMixIn.process_request(self,*p)
1317 return BaseHTTPServer.HTTPServer.process_request(self,*p)
1318 def qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1,log=1,callback_ready=None):
1319 if sys.platform=='win32':
1323 sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
1326 except socket.error, e:
1327 if e[0] == errno.ENOTSOCK:
1329 if fcgi or os.environ.has_key('REQUEST_METHOD'):
1331 fcgi.WSGIServer(wsgiapp,multithreaded=False).run()
1334 print 'Serving on %s:%d'%(ip,port)
1335 s=QWebWSGIServer(wsgiapp,ip=ip,port=port,threaded=threaded,log=log)
1340 except KeyboardInterrupt,e:
1341 sys.excepthook(*sys.exc_info())
1343 #----------------------------------------------------------
1344 # Qweb Documentation
1345 #----------------------------------------------------------
1348 for i in [QWebXml ,QWebHtml ,QWebForm ,QWebURL ,qweb_control ,QWebRequest ,QWebSession ,QWebWSGIServer ,qweb_wsgi_autorun]:
1351 body+='\n\n%s\n%s\n\n%s'%(n,'-'*len(n),d)