├── src ├── Makefile └── setup.py ├── .gitignore ├── TODO ├── test ├── __init__.py ├── base.py ├── test_config.py ├── test_drop_dup_value.py ├── test_qwip.py ├── test_json_request.py └── test_upload.py ├── MANIFEST.in ├── quixote ├── server │ ├── __init__.py │ ├── medusa_http.py │ └── twisted_http.py ├── demo │ ├── demo.conf │ ├── demo.cgi │ ├── demo_scgi.py │ ├── run_cgi.py │ ├── demo_scgi.sh │ ├── __init__.py │ ├── integer_ui.py │ ├── upload.cgi │ ├── pages.ptl │ ├── forms.ptl │ ├── widgets.ptl │ ├── session.ptl │ └── session_demo.cgi ├── form2 │ ├── __init__.py │ ├── css.py │ └── compatibility.py ├── __init__.py ├── ptlc_dump.py ├── form │ └── __init__.py ├── qwip.py ├── qx_distutils.py ├── mod_python_handler.py ├── ptl_import.py ├── html.py ├── errors.py ├── _py_htmltext.py └── sendmail.py ├── doc ├── default.css ├── Makefile ├── ua_test.py ├── multi-threaded.txt ├── INSTALL.txt ├── static-files.txt ├── multi-threaded.html ├── ZPL.txt ├── INSTALL.html ├── static-files.html ├── web-services.txt ├── upload.txt ├── utest_html.py ├── web-services.html ├── upload.html ├── upgrading.txt ├── web-server.txt └── PTL.txt ├── PKG-INFO ├── ACKS ├── setup.py ├── MANIFEST ├── LICENSE └── README /src/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python setup.py build 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/* 2 | *.pyc 3 | *~ 4 | build/* 5 | dist/* 6 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douban/douban-quixote/HEAD/TODO -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Empty file to make this directory a package 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-include *.py *.ptl 2 | include README LICENSE MANIFEST.in MANIFEST CHANGES TODO ACKS 3 | include doc/*.txt doc/*.css doc/Makefile 4 | recursive-include doc *.html 5 | include demo/*.cgi demo/*.conf demo/*.sh 6 | include src/*.c src/Makefile 7 | -------------------------------------------------------------------------------- /quixote/server/__init__.py: -------------------------------------------------------------------------------- 1 | """quixote.server 2 | 3 | This package is for HTTP servers, built using one or another 4 | framework, that publish a Quixote application. These servers can make 5 | it easy to run a small application without having to install and 6 | configure a full-blown Web server such as Apache. 7 | 8 | """ 9 | 10 | __revision__ = "$Id$" 11 | -------------------------------------------------------------------------------- /src/setup.py: -------------------------------------------------------------------------------- 1 | 2 | from distutils.core import setup 3 | from distutils.extension import Extension 4 | 5 | Import = Extension(name="cimport", 6 | sources=["cimport.c"]) 7 | 8 | setup(name = "cimport", 9 | version = "0.1", 10 | description = "Import tools for Python", 11 | author = "Neil Schemenauer", 12 | author_email = "nas@mems-exchange.org", 13 | ext_modules = [Import] 14 | ) 15 | -------------------------------------------------------------------------------- /quixote/demo/demo.conf: -------------------------------------------------------------------------------- 1 | # Config file for the Quixote demo. This ensures that debug and error 2 | # messages will be logged, and that you will see full error information 3 | # in your browser. (The default settings shipped in Quixote's config.py 4 | # module are for security rather than ease of testing/development.) 5 | 6 | ERROR_LOG = "/tmp/quixote-demo-error.log" 7 | DISPLAY_EXCEPTIONS = "plain" 8 | SECURE_ERRORS = 0 9 | FIX_TRAILING_SLASH = 0 10 | -------------------------------------------------------------------------------- /doc/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | Cascading style sheet for the Quixote documentation. 3 | Just overrides what I don't like about the standard docutils 4 | stylesheet. 5 | 6 | $Id: default.css 20217 2003-01-16 20:51:53Z akuchlin $ 7 | */ 8 | 9 | @import url(/misc/docutils.css); 10 | 11 | pre.literal-block, pre.doctest-block { 12 | margin-left: 1em ; 13 | margin-right: 1em ; 14 | background-color: #f4f4f4 } 15 | 16 | tt { background-color: transparent } 17 | -------------------------------------------------------------------------------- /test/base.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import os.path 4 | import unittest 5 | 6 | 7 | p = os.path.dirname(os.path.dirname(__file__)) 8 | sys.path.insert(0, p) 9 | 10 | 11 | 12 | class BaseTestCase(unittest.TestCase): 13 | 14 | def create_publisher(self, ui_cls, conf=None): 15 | import quixote.publish 16 | quixote.publish._publisher = None 17 | publisher = quixote.publish.Publisher(ui_cls()) 18 | if not conf: 19 | conf = {} 20 | publisher.configure(**conf) 21 | return publisher 22 | -------------------------------------------------------------------------------- /quixote/demo/demo.cgi: -------------------------------------------------------------------------------- 1 | #!/www/python/bin/python 2 | 3 | # Example driver script for the Quixote demo: publishes the contents of 4 | # the quixote.demo package. 5 | 6 | from quixote import enable_ptl, Publisher 7 | 8 | # Install the import hook that enables PTL modules. 9 | enable_ptl() 10 | 11 | # Create a Publisher instance 12 | app = Publisher('quixote.demo') 13 | 14 | # (Optional step) Read a configuration file 15 | app.read_config("demo.conf") 16 | 17 | # Open the configured log files 18 | app.setup_logs() 19 | 20 | # Enter the publishing main loop 21 | app.publish_cgi() 22 | -------------------------------------------------------------------------------- /test/test_config.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from quixote.publish import Publisher 3 | from quixote.http_request import HTTPJSONRequest 4 | from cStringIO import StringIO 5 | 6 | class TestConfig(TestCase): 7 | def test_config_for_application_json_support(self): 8 | pub = Publisher('__main__') 9 | self.assertFalse(pub.config.support_application_json) 10 | 11 | req = pub.create_request(StringIO(), {'CONTENT_TYPE': "application/json"}) 12 | self.assertFalse(isinstance(req, HTTPJSONRequest)) 13 | 14 | pub.configure(SUPPORT_APPLICATION_JSON=1) 15 | 16 | req = pub.create_request(StringIO(), {'CONTENT_TYPE': "application/json"}) 17 | self.assertTrue(isinstance(req, HTTPJSONRequest)) 18 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile to convert Quixote docs to HTML 3 | # 4 | # $Id: Makefile 20217 2003-01-16 20:51:53Z akuchlin $ 5 | # 6 | 7 | TXT_FILES = $(wildcard *.txt) 8 | HTML_FILES = $(filter-out ZPL%,$(TXT_FILES:%.txt=%.html)) 9 | 10 | RST2HTML = /www/python/bin/rst2html 11 | RST2HTML_OPTS = -o us-ascii 12 | 13 | DEST_HOST = staging.mems-exchange.org 14 | DEST_DIR = /www/www-docroot/software/quixote/doc 15 | 16 | SS = default.css 17 | 18 | %.html: %.txt 19 | $(RST2HTML) $(RST2HTML_OPTS) $< $@ 20 | 21 | all: $(HTML_FILES) 22 | 23 | clean: 24 | rm -f $(HTML_FILES) 25 | 26 | install: 27 | rsync -vptgo *.html $(SS) $(DEST_HOST):$(DEST_DIR) 28 | 29 | local-install: 30 | dir=`pwd` ; \ 31 | cd $(DEST_DIR) && ln -sf $$dir/*.html $$dir/$(SS) . 32 | -------------------------------------------------------------------------------- /doc/ua_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Test Quixote's ability to parse the "User-Agent" header, ie. 4 | # the 'guess_browser_version()' method of HTTPRequest. 5 | # 6 | # Reads User-Agent strings on stdin, and writes Quixote's interpretation 7 | # of each on stdout. This is *not* an automated test! 8 | 9 | import sys, os 10 | from copy import copy 11 | from quixote.http_request import HTTPRequest 12 | 13 | env = copy(os.environ) 14 | file = sys.stdin 15 | while 1: 16 | line = file.readline() 17 | if not line: 18 | break 19 | if line[-1] == "\n": 20 | line = line[:-1] 21 | 22 | env["HTTP_USER_AGENT"] = line 23 | req = HTTPRequest(None, env) 24 | (name, version) = req.guess_browser_version() 25 | if name is None: 26 | print "%s -> ???" % line 27 | else: 28 | print "%s -> (%s, %s)" % (line, name, version) 29 | -------------------------------------------------------------------------------- /quixote/form2/__init__.py: -------------------------------------------------------------------------------- 1 | """$URL: svn+ssh://svn/repos/trunk/quixote/form2/__init__.py $ 2 | $Id$ 3 | 4 | The web interface framework, consisting of Form and Widget base classes 5 | (and a bunch of standard widget classes recognized by Form). 6 | Application developers will typically create a Form instance each 7 | form in their application; each form object will contain a number 8 | of widget objects. Custom widgets can be created by inheriting 9 | and/or composing the standard widget classes. 10 | """ 11 | 12 | from quixote.form2.form import Form, FormTokenWidget 13 | from quixote.form2.widget import Widget, StringWidget, FileWidget, \ 14 | PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ 15 | SingleSelectWidget, SelectWidget, OptionSelectWidget, \ 16 | MultipleSelectWidget, SubmitWidget, HiddenWidget, \ 17 | FloatWidget, IntWidget, subname, WidgetValueError, CompositeWidget, \ 18 | WidgetList 19 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: Quixote 3 | Version: 1.2 4 | Summary: A highly Pythonic Web application framework 5 | Home-page: http://www.mems-exchange.org/software/quixote/ 6 | Author: MEMS Exchange 7 | Author-email: quixote@mems-exchange.org 8 | License: CNRI Open Source License (see LICENSE.txt) 9 | Provides: quixote-1.2 10 | Provides: quixote.demo-1.2 11 | Provides: quixote.form-1.2 12 | Provides: quixote.form2-1.2 13 | Provides: quixote.server-1.2 14 | Download-URL: http://www.mems-exchange.org/software/files/quixote/Quixote-1.2.tar.gz 15 | Description: UNKNOWN 16 | Platform: UNKNOWN 17 | Classifier: Development Status :: 5 - Production/Stable 18 | Classifier: Environment :: Web Environment 19 | Classifier: License :: OSI Approved :: Python License (CNRI Python License) 20 | Classifier: Intended Audience :: Developers 21 | Classifier: Operating System :: Unix 22 | Classifier: Operating System :: Microsoft :: Windows 23 | Classifier: Operating System :: MacOS :: MacOS X 24 | Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content 25 | -------------------------------------------------------------------------------- /quixote/demo/demo_scgi.py: -------------------------------------------------------------------------------- 1 | #!/www/python/bin/python 2 | 3 | # Example SCGI driver script for the Quixote demo: publishes the contents of 4 | # the quixote.demo package. To use this script with mod_scgi and Apache 5 | # add the following section of your Apache config file: 6 | # 7 | # 8 | # SCGIServer 127.0.0.1 4000 9 | # SCGIHandler On 10 | # 11 | 12 | 13 | from scgi.quixote_handler import QuixoteHandler, main 14 | from quixote import enable_ptl, Publisher 15 | 16 | class DemoPublisher(Publisher): 17 | def __init__(self, *args, **kwargs): 18 | Publisher.__init__(self, *args, **kwargs) 19 | 20 | # (Optional step) Read a configuration file 21 | self.read_config("demo.conf") 22 | 23 | # Open the configured log files 24 | self.setup_logs() 25 | 26 | 27 | class DemoHandler(QuixoteHandler): 28 | publisher_class = DemoPublisher 29 | root_namespace = "quixote.demo" 30 | prefix = "/qdemo" 31 | 32 | 33 | # Install the import hook that enables PTL modules. 34 | enable_ptl() 35 | main(DemoHandler) 36 | -------------------------------------------------------------------------------- /ACKS: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | ================ 3 | 4 | The Quixote developer team would like to thank everybody who 5 | contributed in any way, with code, hints, bug reports, ideas, moral 6 | support, endorsement, or even complaints. Listed in alphabetical 7 | order: 8 | 9 | David Ascher 10 | Anton Benard 11 | Titus Brown 12 | Oleg Broytmann 13 | David M. Cooke 14 | Jonathan Corbet 15 | Herman Cuppens 16 | Toby Dickenson 17 | Ray Drew 18 | Jim Dukarm 19 | Quinn Dunkan 20 | Robin Dunn (original author of fcgi.py) 21 | Jon Dyte 22 | David Edwards 23 | Graham Fawcett 24 | Jim Fulton (original author of the *Request and *Response classes) 25 | David Goodger 26 | Neal M. Holtz 27 | Kaweh Kazemi 28 | Shahms E. King 29 | A.M. Kuchling 30 | Erno Kuusela 31 | Nicola Larosa 32 | Hamish Lawson 33 | Patrick K. O'Brien 34 | Brendan T O'Connor 35 | Ed Overly 36 | Paul Richardson 37 | Jeff Rush 38 | Neil Schemenauer 39 | Jason Sibre 40 | Gregory P. Smith 41 | Mikhail Sobolev 42 | Johann Visagie 43 | Greg Ward 44 | The whole gang at the Zope Corporation 45 | -------------------------------------------------------------------------------- /quixote/demo/run_cgi.py: -------------------------------------------------------------------------------- 1 | # This is a simple script that makes it easy to write one file CGI 2 | # applications that use Quixote. To use, add the following line to the top 3 | # of your CGI script: 4 | # 5 | # #!/usr/local/bin/python /run_cgi.py 6 | # 7 | # Your CGI script becomes the root namespace and you may use PTL syntax 8 | # inside the script. Errors will go to stderr and should end up in the server 9 | # error log. 10 | # 11 | # Note that this will be quite slow since the script will be recompiled on 12 | # every hit. If you are using Apache with mod_fastcgi installed you should be 13 | # able to use .fcgi as an extension instead of .cgi and get much better 14 | # performance. Maybe someday I will write code that caches the compiled 15 | # script on the filesystem. :-) 16 | 17 | import sys 18 | import new 19 | from quixote import enable_ptl, ptl_compile, Publisher 20 | 21 | enable_ptl() 22 | filename = sys.argv[1] 23 | root_code = ptl_compile.compile_template(open(filename), filename) 24 | root = new.module("root") 25 | root.__file__ = filename 26 | root.__name__ = "root" 27 | exec root_code in root.__dict__ 28 | p = Publisher(root) 29 | p.publish_cgi() 30 | -------------------------------------------------------------------------------- /quixote/__init__.py: -------------------------------------------------------------------------------- 1 | """quixote 2 | $HeadURL: svn+ssh://svn/repos/trunk/quixote/__init__.py $ 3 | $Id$ 4 | 5 | A highly Pythonic web application framework. 6 | """ 7 | 8 | __revision__ = "$Id$" 9 | 10 | __version__ = "1.2" 11 | 12 | __all__ = ['Publisher', 13 | 'get_publisher', 'get_request', 'get_session', 'get_user', 14 | 'get_path', 'enable_ptl', 'redirect'] 15 | 16 | 17 | # These are frequently needed by Quixote applications, so make them easy 18 | # to get at. 19 | from quixote.publish import Publisher, \ 20 | get_publisher, get_request, get_path, redirect, \ 21 | get_session, get_session_manager, get_user 22 | 23 | # Can't think of anywhere better to put this, so here it is. 24 | def enable_ptl(): 25 | """ 26 | Installs the import hooks needed to import PTL modules. This must 27 | be done explicitly because not all Quixote applications need to use 28 | PTL, and import hooks are deep magic that can cause all sorts of 29 | mischief and deeply confuse innocent bystanders. Thus, we avoid 30 | invoking them behind the programmer's back. One known problem is 31 | that, if you use ZODB, you must import ZODB before calling this 32 | function. 33 | """ 34 | from quixote import ptl_import 35 | ptl_import.install() 36 | -------------------------------------------------------------------------------- /quixote/demo/demo_scgi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Example init.d script for demo_scgi.py server 4 | 5 | PATH=/bin:/usr/bin:/usr/local/bin 6 | DAEMON=./demo_scgi.py 7 | PIDFILE=/var/tmp/demo_scgi.pid 8 | 9 | NAME=`basename $DAEMON` 10 | case "$1" in 11 | start) 12 | if [ -f $PIDFILE ]; then 13 | if ps -p `cat $PIDFILE` > /dev/null 2>&1 ; then 14 | echo "$NAME appears to be already running ($PIDFILE exists)." 15 | exit 1 16 | else 17 | echo "$PIDFILE exists, but appears to be obsolete; removing it" 18 | rm $PIDFILE 19 | fi 20 | fi 21 | 22 | echo -n "Starting $NAME: " 23 | env -i PATH=$PATH \ 24 | $DAEMON -P $PIDFILE -l /var/tmp/quixote-error.log 25 | echo "done" 26 | ;; 27 | 28 | stop) 29 | if [ -f $PIDFILE ]; then 30 | echo -n "Stopping $NAME: " 31 | kill `cat $PIDFILE` 32 | echo "done" 33 | if ps -p `cat $PIDFILE` > /dev/null 2>&1 ; then 34 | echo "$NAME is still running, not removing $PIDFILE" 35 | else 36 | rm -f $PIDFILE 37 | fi 38 | else 39 | echo "$NAME does not appear to be running ($PIDFILE doesn't exist)." 40 | fi 41 | ;; 42 | 43 | restart) 44 | $0 stop 45 | $0 start 46 | ;; 47 | 48 | *) 49 | echo "Usage: $0 {start|stop|restart}" 50 | exit 1 51 | ;; 52 | esac 53 | -------------------------------------------------------------------------------- /quixote/ptlc_dump.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | Dump the information contained in a compiled PTL file. Based on the 4 | dumppyc.py script in the Tools/compiler directory of the Python 5 | distribution. 6 | """ 7 | 8 | __revision__ = "$Id$" 9 | 10 | import marshal 11 | import dis 12 | import types 13 | 14 | from ptl_compile import PTLC_MAGIC 15 | 16 | def dump(obj): 17 | print obj 18 | for attr in dir(obj): 19 | print "\t", attr, repr(getattr(obj, attr)) 20 | 21 | def loadCode(path): 22 | f = open(path) 23 | magic = f.read(len(PTLC_MAGIC)) 24 | if magic != PTLC_MAGIC: 25 | raise ValueError, 'bad .ptlc magic for file "%s"' % path 26 | mtime = marshal.load(f) 27 | co = marshal.load(f) 28 | f.close() 29 | return co 30 | 31 | def walk(co, match=None): 32 | if match is None or co.co_name == match: 33 | dump(co) 34 | print 35 | dis.dis(co) 36 | for obj in co.co_consts: 37 | if type(obj) == types.CodeType: 38 | walk(obj, match) 39 | 40 | def main(filename, codename=None): 41 | co = loadCode(filename) 42 | walk(co, codename) 43 | 44 | if __name__ == "__main__": 45 | import sys 46 | if len(sys.argv) == 3: 47 | filename, codename = sys.argv[1:] 48 | else: 49 | filename = sys.argv[1] 50 | codename = None 51 | main(filename, codename) 52 | -------------------------------------------------------------------------------- /doc/multi-threaded.txt: -------------------------------------------------------------------------------- 1 | Multi-Threaded Quixote Applications 2 | =================================== 3 | 4 | Starting with Quixote 0.6, it's possible to write multi-threaded Quixote 5 | applications. In previous versions, Quixote stored the current 6 | HTTPRequest object in a global variable, meaning that processing 7 | multiple requests in the same process simultaneously was impossible. 8 | 9 | However, the Publisher class as shipped still can't handle multiple 10 | simultaneous requests; you'll need to subclass Publisher to make it 11 | re-entrant. Here's a starting point:: 12 | 13 | import thread 14 | from quixote.publish import Publisher 15 | 16 | [...] 17 | 18 | class ThreadedPublisher (Publisher): 19 | def __init__ (self, root_namespace, config=None): 20 | Publisher.__init__(self, root_namespace, config) 21 | self._request_dict = {} 22 | 23 | def _set_request(self, request): 24 | self._request_dict[thread.get_ident()] = request 25 | 26 | def _clear_request(self): 27 | try: 28 | del self._request_dict[thread.get_ident()] 29 | except KeyError: 30 | pass 31 | 32 | def get_request(self): 33 | return self._request_dict.get(thread.get_ident()) 34 | 35 | Using ThreadedPublisher, you now have one current request per thread, 36 | rather than one for the entire process. 37 | 38 | 39 | $Id: multi-threaded.txt 20217 2003-01-16 20:51:53Z akuchlin $ 40 | -------------------------------------------------------------------------------- /test/test_drop_dup_value.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import unittest 5 | 6 | from cStringIO import StringIO 7 | from urllib import urlencode 8 | 9 | from base import BaseTestCase 10 | 11 | from quixote.http_request import HTTPRequest 12 | 13 | 14 | class ParseFormTestCase(BaseTestCase): 15 | 16 | def test_dup_var_should_be_removed(self): 17 | fields_qs = [ 18 | ('a', 1), 19 | ('b', 1), 20 | ] 21 | 22 | fields_form = [ 23 | ('a', 1), 24 | ('b', 2), 25 | ] 26 | 27 | qs = urlencode(fields_qs) 28 | body = urlencode(fields_form) 29 | stdin = StringIO(body) 30 | 31 | env = { 32 | 'SERVER_PROTOCOL': 'HTTP/1.0', 33 | 'REQUEST_METHOD': 'POST', 34 | 'PATH_INFO': '/', 35 | 'CONTENT_TYPE': "application/x-www-form-urlencoded", 36 | 'CONTENT_LENGTH': str(len(body)), 37 | 'QUERY_STRING': qs, 38 | } 39 | 40 | 41 | req = HTTPRequest(stdin, env) 42 | req.process_inputs() 43 | 44 | self.assertEqual(req.form, {'a': ['1', '1'], 'b': ['2', '1']}) 45 | 46 | self.assertEqual(req.get_form_var('a'), '1') 47 | self.assertEqual(req.get_form_list_var('a'), ['1', '1']) 48 | 49 | self.assertEqual(req.get_form_var('b'), ['2','1']) 50 | self.assertEqual(req.get_form_list_var('b'), ['2','1']) 51 | 52 | 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | 57 | -------------------------------------------------------------------------------- /test/test_qwip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import unittest 5 | 6 | from webtest import TestApp 7 | from webtest import Upload 8 | 9 | from base import BaseTestCase 10 | 11 | from quixote.qwip import QWIP 12 | from quixote.publish import Publisher 13 | 14 | 15 | class UITest(object): 16 | _q_exports = ['', 'upload', 'form'] 17 | 18 | def _q_index(self, req): 19 | return "hello, world" 20 | 21 | def form(self, req): 22 | return '
' 23 | 24 | def upload(self, req): 25 | if req.get_method() == 'POST': 26 | upload = req.get_form_var("file") 27 | return open(upload.tmp_filename).read() 28 | 29 | 30 | class QWIPTestCase(BaseTestCase): 31 | 32 | def setUp(self): 33 | conf = dict(UPLOAD_DIR='/tmp/') 34 | self.app = TestApp(QWIP(self.create_publisher(UITest, conf))) 35 | 36 | def test_basic_page(self): 37 | resp = self.app.get('/') 38 | assert resp.status == '200 OK' 39 | assert resp.content_type == 'text/html' 40 | assert resp.body == "hello, world" 41 | 42 | def test_form(self): 43 | res = self.app.get('/form') 44 | form = res.form 45 | data = 'test'*10 46 | form['file'] = Upload('test.txt', data) 47 | resp = form.submit(content_type="multipart/form-data") 48 | assert resp.body == data 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /quixote/form2/css.py: -------------------------------------------------------------------------------- 1 | 2 | BASIC_FORM_CSS = """\ 3 | form.quixote div.title { 4 | font-weight: bold; 5 | } 6 | 7 | form.quixote br.submit, 8 | form.quixote br.widget, 9 | br.quixoteform { 10 | clear: left; 11 | } 12 | 13 | form.quixote div.submit br.widget { 14 | display: none; 15 | } 16 | 17 | form.quixote div.widget { 18 | float: left; 19 | padding: 4px; 20 | padding-right: 1em; 21 | margin-bottom: 6px; 22 | } 23 | 24 | /* pretty forms (attribute selector hides from broken browsers (e.g. IE) */ 25 | form.quixote[action] { 26 | float: left; 27 | } 28 | 29 | form.quixote[action] > div.widget { 30 | float: none; 31 | } 32 | 33 | form.quixote[action] > br.widget { 34 | display: none; 35 | } 36 | 37 | form.quixote div.widget div.widget { 38 | padding: 0; 39 | margin-bottom: 0; 40 | } 41 | 42 | form.quixote div.SubmitWidget { 43 | float: left 44 | } 45 | 46 | form.quixote div.content { 47 | margin-left: 0.6em; /* indent content */ 48 | } 49 | 50 | form.quixote div.content div.content { 51 | margin-left: 0; /* indent content only for top-level widgets */ 52 | } 53 | 54 | form.quixote div.error { 55 | color: #c00; 56 | font-size: small; 57 | margin-top: .1em; 58 | } 59 | 60 | form.quixote div.hint { 61 | font-size: small; 62 | font-style: italic; 63 | margin-top: .1em; 64 | } 65 | 66 | form.quixote div.errornotice { 67 | color: #c00; 68 | padding: 0.5em; 69 | margin: 0.5em; 70 | } 71 | 72 | form.quixote div.FormTokenWidget, 73 | form.quixote.div.HiddenWidget { 74 | display: none; 75 | } 76 | """ 77 | -------------------------------------------------------------------------------- /quixote/demo/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | _q_exports = ["simple", "error", "publish_error", "widgets", 3 | "form_demo", "dumpreq", "srcdir", 4 | ("favicon.ico", "q_ico")] 5 | 6 | import sys 7 | from quixote.demo.pages import _q_index, _q_exception_handler, dumpreq 8 | from quixote.demo.widgets import widgets 9 | from quixote.demo.integer_ui import IntegerUI 10 | from quixote.errors import PublishError 11 | from quixote.util import StaticDirectory, StaticFile 12 | 13 | def simple(request): 14 | # This function returns a plain text document, not HTML. 15 | request.response.set_content_type("text/plain") 16 | return "This is the Python function 'quixote.demo.simple'.\n" 17 | 18 | def error(request): 19 | raise ValueError, "this is a Python exception" 20 | 21 | def publish_error(request): 22 | raise PublishError(public_msg="Publishing error raised by publish_error") 23 | 24 | def _q_lookup(request, component): 25 | return IntegerUI(request, component) 26 | 27 | def _q_resolve(component): 28 | # _q_resolve() is a hook that can be used to import only 29 | # when it's actually accessed. This can be used to make 30 | # start-up of your application faster, because it doesn't have 31 | # to import every single module when it starts running. 32 | if component == 'form_demo': 33 | from quixote.demo.forms import form_demo 34 | return form_demo 35 | 36 | # Get current directory 37 | import os 38 | from quixote.demo import forms 39 | curdir = os.path.dirname(forms.__file__) 40 | srcdir = StaticDirectory(curdir, list_directory=1) 41 | q_ico = StaticFile(os.path.join(curdir, 'q.ico')) 42 | -------------------------------------------------------------------------------- /quixote/form/__init__.py: -------------------------------------------------------------------------------- 1 | """quixote.form 2 | 3 | The web interface framework, consisting of Form and Widget base classes 4 | (and a bunch of standard widget classes recognized by Form). 5 | Application developers will typically create a Form subclass for each 6 | form in their application; each form object will contain a number 7 | of widget objects. Custom widgets can be created by inheriting 8 | and/or composing the standard widget classes. 9 | """ 10 | 11 | # created 2000/09/19 - 22, GPW 12 | 13 | __revision__ = "$Id$" 14 | 15 | from quixote.form.form import Form, register_widget_class, FormTokenWidget 16 | from quixote.form.widget import Widget, StringWidget, FileWidget, \ 17 | PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ 18 | SingleSelectWidget, SelectWidget, OptionSelectWidget, \ 19 | MultipleSelectWidget, ListWidget, SubmitButtonWidget, HiddenWidget, \ 20 | FloatWidget, IntWidget, CollapsibleListWidget, FormValueError 21 | 22 | # Register the standard widget classes 23 | register_widget_class(StringWidget) 24 | register_widget_class(FileWidget) 25 | register_widget_class(PasswordWidget) 26 | register_widget_class(TextWidget) 27 | register_widget_class(CheckboxWidget) 28 | register_widget_class(RadiobuttonsWidget) 29 | register_widget_class(SingleSelectWidget) 30 | register_widget_class(OptionSelectWidget) 31 | register_widget_class(MultipleSelectWidget) 32 | register_widget_class(ListWidget) 33 | register_widget_class(SubmitButtonWidget) 34 | register_widget_class(HiddenWidget) 35 | register_widget_class(FloatWidget) 36 | register_widget_class(IntWidget) 37 | register_widget_class(CollapsibleListWidget) 38 | -------------------------------------------------------------------------------- /quixote/qwip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*-coding=utf8-*- 3 | 4 | ''' 5 | QWIP: WSGI COMPATIBILITY WRAPPER FOR QUIXOTE 6 | ''' 7 | 8 | from quixote.publish import set_publisher 9 | 10 | class QWIP(object): 11 | """I make a Quixote Publisher object look like a WSGI application.""" 12 | 13 | def __init__(self, publisher): 14 | self.publisher = publisher 15 | 16 | def __call__(self, env, start_response): 17 | """I am called for each request.""" 18 | # maybe more than one publisher per app, need to set publisher per request 19 | set_publisher(self.publisher) 20 | if env.get('wsgi.multithread') and not \ 21 | getattr(self.publisher, 'is_thread_safe', False): 22 | reason = "%r is not thread safe" % self.publisher 23 | raise AssertionError(reason) 24 | if 'REQUEST_URI' not in env: 25 | env['REQUEST_URI'] = env['SCRIPT_NAME'] + env['PATH_INFO'] 26 | if env.get('QUERY_STRING', ''): 27 | env['REQUEST_URI'] += '?%s' %env['QUERY_STRING'] 28 | if env['wsgi.url_scheme'] == 'https': 29 | env['HTTPS'] = 'on' 30 | input = env['wsgi.input'] 31 | request = self.publisher.create_request(input, env) 32 | output = self.publisher.process_request(request, env) 33 | request.response.set_body(output) 34 | response = request.response 35 | headers = response.generate_headers() 36 | headers = [(str(key), str(value)) for key, value in headers] 37 | key, status = headers.pop(0) 38 | assert key == 'Status' 39 | start_response(status, headers) 40 | self.publisher._clear_request() 41 | return [response.body,] # Iterable object! 42 | -------------------------------------------------------------------------------- /test/test_json_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf8 -*- 2 | from unittest import TestCase 3 | from quixote.errors import QueryError 4 | from quixote.http_request import HTTPJSONRequest 5 | from cStringIO import StringIO 6 | from json import loads, dumps 7 | 8 | class TestJSONRequest(TestCase): 9 | def setUp(self): 10 | self.fake_environ = {'CONTENT_LENGTH': '291', 11 | 'CONTENT_TYPE': 'application/json; charset=utf-8'} 12 | 13 | def test_process_input_of_json_object(self): 14 | json = dumps({ 15 | "1": u"哈哈哈", 16 | u"a": { 17 | "b": { 18 | "二": u"我", 19 | }, 20 | }, 21 | "c": [u"我", u"你", u"c"], 22 | }, ensure_ascii=True) 23 | 24 | req = HTTPJSONRequest(StringIO(json), self.fake_environ) 25 | req.process_inputs() 26 | self.assertEqual(req.json, { 27 | "1": "哈哈哈", 28 | "a": { 29 | "b": { 30 | "二": "我", 31 | }, 32 | }, 33 | "c": ["我", "你", "c"], 34 | }) 35 | self.assertEqual(req.json, req.form) 36 | 37 | def test_process_input_of_json_array(self): 38 | json = dumps([ 39 | u"哈哈哈", 40 | u"a", 41 | u"我", 42 | u"你", 43 | u"c", 44 | ], ensure_ascii=True) 45 | 46 | req = HTTPJSONRequest(StringIO(json), self.fake_environ) 47 | req.process_inputs() 48 | self.assertEqual(req.json, [ 49 | "哈哈哈", 50 | "a", 51 | "我", 52 | "你", 53 | "c", 54 | ]) 55 | 56 | def test_query_error(self): 57 | req = HTTPJSONRequest(StringIO(''), self.fake_environ) 58 | self.assertRaises(QueryError, req.process_inputs) 59 | -------------------------------------------------------------------------------- /doc/INSTALL.txt: -------------------------------------------------------------------------------- 1 | Installing Quixote 2 | ================== 3 | 4 | Best-case scenario 5 | ------------------ 6 | 7 | If you are using Python 2.2 or later, and have never installed Quixote 8 | with your current version of Python, you're in luck. Just run 9 | 10 | python setup.py install 11 | 12 | and you're done. Proceed to the demo documentation to learn how to get 13 | Quixote working. 14 | 15 | If you're using an older Python, or if you're upgrading from an older 16 | Quixote version, read on. 17 | 18 | 19 | Upgrading from an older Quixote version 20 | --------------------------------------- 21 | 22 | We strongly recommend that you remove any old Quixote version before 23 | installing a new one. First, find out where your old Quixote 24 | installation is: 25 | 26 | python -c "import os, quixote; print os.path.dirname(quixote.__file__)" 27 | 28 | and then remove away the reported directory. (If the import fails, then 29 | you don't have an existing Quixote installation.) 30 | 31 | Then proceed as above: 32 | 33 | python setup.py install 34 | 35 | 36 | Using Quixote with Python 2.0 or 2.1 37 | ------------------------------------ 38 | 39 | If you are using Python 2.0 or 2.1 then you need to install the 40 | ``compiler`` package from the Python source distribution. The 41 | ``compiler`` package is for parsing Python source code and generating 42 | Python bytecode, and the PTL compiler is built on top of it. With 43 | Python 2.0 and 2.1, this package was included in Python's source 44 | distribution, but not installed as part of the standard library. 45 | 46 | Assuming your Python source distribution is in ``/tmp/Python-2.1.2``:: 47 | 48 | cd /tmp/Python-2.1.2/Tools/compiler 49 | python setup.py install 50 | 51 | (Obviously, you'll have to adjust this to reflect your Python version 52 | and where you kept the source distribution after installing Python.) 53 | 54 | -------------------------------------------------------------------------------- /test/test_upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import sys 5 | import os 6 | import os.path 7 | import unittest 8 | import shutil 9 | 10 | from webtest import TestApp 11 | from webtest import Upload 12 | 13 | from base import BaseTestCase 14 | 15 | from quixote.qwip import QWIP 16 | from quixote.publish import Publisher 17 | 18 | 19 | upload_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'upload') 20 | if not os.path.exists(upload_dir): 21 | os.mkdir(upload_dir) 22 | for name in os.listdir(upload_dir): 23 | path = os.path.join(upload_dir, name) 24 | try: 25 | os.remove(path) 26 | except OSError: 27 | shutil.rmtree(path, ignore_errors=True) 28 | 29 | 30 | class UITest(object): 31 | _q_exports = ['upload', 'form'] 32 | 33 | def form(self, req): 34 | return '
' 35 | 36 | def upload(self, req): 37 | if req.get_method() == 'POST': 38 | upload = req.get_form_var("file") 39 | fname = upload.orig_filename 40 | data = open(upload.tmp_filename).read() 41 | return '%s %s' % (fname, data) 42 | 43 | 44 | class QWIPTestCase(BaseTestCase): 45 | def setUp(self): 46 | conf = dict(UPLOAD_DIR=upload_dir) 47 | self.app = TestApp(QWIP(self.create_publisher(UITest, conf))) 48 | 49 | def test_form(self): 50 | res = self.app.get('/form') 51 | form = res.form 52 | fname = 'test.txt' 53 | data = 'test'*10 54 | form['file'] = Upload(fname, data) 55 | resp = form.submit(content_type="multipart/form-data") 56 | assert resp.body == '%s %s' % (fname, data) 57 | assert len(os.listdir(upload_dir)) == 0 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /quixote/demo/integer_ui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from quixote.errors import TraversalError 3 | 4 | def fact(n): 5 | f = 1L 6 | while n > 1: 7 | f *= n 8 | n -= 1 9 | return f 10 | 11 | class IntegerUI: 12 | 13 | _q_exports = ["factorial", "prev", "next"] 14 | 15 | def __init__(self, request, component): 16 | try: 17 | self.n = int(component) 18 | except ValueError, exc: 19 | raise TraversalError(str(exc)) 20 | 21 | def factorial(self, request): 22 | if self.n > 10000: 23 | sys.stderr.write("warning: possible denial-of-service attack " 24 | "(request for factorial(%d))\n" % self.n) 25 | request.response.set_header("content-type", "text/plain") 26 | return "%d! = %d\n" % (self.n, fact(self.n)) 27 | 28 | def _q_index(self, request): 29 | return """\ 30 | 31 | The Number %d 32 | 33 | You have selected the integer %d.

34 | 35 | You can compute its factorial (%d!)

36 | 37 | Or, you can visit the web page for the 38 | previous or 39 | next integer.

40 | 41 | Or, you can use redirects to visit the 42 | previous or 43 | next integer. This makes 44 | it a bit easier to generate this HTML code, but 45 | it's less efficient -- your browser has to go through 46 | two request/response cycles. And someone still 47 | has to generate the URLs for the previous/next 48 | pages -- only now it's done in the prev() 49 | and next() methods for this integer.

50 | 51 | 52 | 53 | """ % (self.n, self.n, self.n, self.n-1, self.n+1) 54 | 55 | def prev(self, request): 56 | return request.redirect("../%d/" % (self.n-1)) 57 | 58 | def next(self, request): 59 | return request.redirect("../%d/" % (self.n+1)) 60 | -------------------------------------------------------------------------------- /doc/static-files.txt: -------------------------------------------------------------------------------- 1 | Examples of serving static files 2 | ================================ 3 | 4 | The ``quixote.util`` module includes classes for making files and 5 | directories available as Quixote resources. Here are some examples. 6 | 7 | 8 | Publishing a Single File 9 | ------------------------ 10 | 11 | The ``StaticFile`` class makes an individual filesystem file (possibly 12 | a symbolic link) available. You can also specify the MIME type and 13 | encoding of the file; if you don't specify this, the MIME type will be 14 | guessed using the standard Python ``mimetypes.guess_type()`` function. 15 | The default action is to not follow symbolic links, but this behaviour 16 | can be changed using the ``follow_symlinks`` parameter. 17 | 18 | The following example publishes a file with the URL ``.../stylesheet_css``:: 19 | 20 | # 'stylesheet_css' must be in the _q_exports list 21 | _q_exports = [ ..., 'stylesheet_css', ...] 22 | 23 | stylesheet_css = StaticFile( 24 | "/htdocs/legacy_app/stylesheet.css", 25 | follow_symlinks=1, mime_type="text/css") 26 | 27 | 28 | If you want the URL of the file to have a ``.css`` extension, you use 29 | the external to internal name mapping feature of ``_q_exports``. For 30 | example:: 31 | 32 | _q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...] 33 | 34 | 35 | 36 | Publishing a Directory 37 | ---------------------- 38 | 39 | Publishing a directory is similar. The ``StaticDirectory`` class 40 | makes a complete filesystem directory available. Again, the default 41 | behaviour is to not follow symlinks. You can also request that the 42 | ``StaticDirectory`` object cache information about the files in 43 | memory so that it doesn't try to guess the MIME type on every hit. 44 | 45 | This example publishes the ``notes/`` directory:: 46 | 47 | _q_exports = [ ..., 'notes', ...] 48 | 49 | notes = StaticDirectory("/htdocs/legacy_app/notes") 50 | 51 | 52 | -------------------------------------------------------------------------------- /doc/multi-threaded.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Multi-Threaded Quixote Applications 8 | 9 | 10 | 11 |

12 |

Multi-Threaded Quixote Applications

13 |

Starting with Quixote 0.6, it's possible to write multi-threaded Quixote 14 | applications. In previous versions, Quixote stored the current 15 | HTTPRequest object in a global variable, meaning that processing 16 | multiple requests in the same process simultaneously was impossible.

17 |

However, the Publisher class as shipped still can't handle multiple 18 | simultaneous requests; you'll need to subclass Publisher to make it 19 | re-entrant. Here's a starting point:

20 |
21 | import thread
22 | from quixote.publish import Publisher
23 | 
24 | [...]
25 | 
26 | class ThreadedPublisher (Publisher):
27 |     def __init__ (self, root_namespace, config=None):
28 |         Publisher.__init__(self, root_namespace, config)
29 |         self._request_dict = {}
30 | 
31 |     def _set_request(self, request):
32 |         self._request_dict[thread.get_ident()] = request
33 | 
34 |     def _clear_request(self):
35 |         try:
36 |             del self._request_dict[thread.get_ident()]
37 |         except KeyError:
38 |             pass
39 | 
40 |     def get_request(self):
41 |         return self._request_dict.get(thread.get_ident())
42 | 
43 |

Using ThreadedPublisher, you now have one current request per thread, 44 | rather than one for the entire process.

45 |

$Id: multi-threaded.txt 20217 2003-01-16 20:51:53Z akuchlin $

46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /quixote/demo/upload.cgi: -------------------------------------------------------------------------------- 1 | #!/www/python/bin/python 2 | 3 | # Simple demo of HTTP upload with Quixote. Also serves as an example 4 | # of how to put a (simple) Quixote application into a single file. 5 | 6 | __revision__ = "$Id: upload.cgi 21182 2003-03-17 21:46:52Z gward $" 7 | 8 | import os 9 | import stat 10 | from quixote import Publisher 11 | from quixote.html import html_quote 12 | 13 | _q_exports = ['receive'] 14 | 15 | def header (title): 16 | return '''\ 17 | %s 18 | 19 | ''' % title 20 | 21 | def footer (): 22 | return '\n' 23 | 24 | def _q_index (request): 25 | return header("Quixote Upload Demo") + '''\ 26 |
29 | Your name:
30 |
31 | File to upload:
32 |
33 | 34 |
35 | ''' + footer() 36 | 37 | def receive (request): 38 | result = [] 39 | name = request.form.get("name") 40 | if name: 41 | result.append("

Thanks, %s!

" % html_quote(name)) 42 | 43 | upload = request.form.get("upload") 44 | size = os.stat(upload.tmp_filename)[stat.ST_SIZE] 45 | if not upload.base_filename or size == 0: 46 | title = "Empty Upload" 47 | result.append("

You appear not to have uploaded anything.

") 48 | else: 49 | title = "Upload Received" 50 | result.append("

You just uploaded %s (%d bytes)
" 51 | % (html_quote(upload.base_filename), size)) 52 | result.append("which is temporarily stored in %s.

" 53 | % html_quote(upload.tmp_filename)) 54 | 55 | return header(title) + "\n".join(result) + footer() 56 | 57 | def main (): 58 | pub = Publisher('__main__') 59 | pub.read_config("demo.conf") 60 | pub.configure(UPLOAD_DIR="/tmp/quixote-upload-demo") 61 | pub.setup_logs() 62 | pub.publish_cgi() 63 | 64 | main() 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #$HeadURL: svn+ssh://svn/repos/trunk/quixote/setup.py $ 3 | #$Id$ 4 | 5 | # Setup script for Quixote 6 | 7 | __revision__ = "$Id$" 8 | 9 | import sys, os 10 | from setuptools import setup, Extension 11 | from quixote.qx_distutils import qx_build_py 12 | 13 | # a fast htmltext type 14 | htmltext = Extension(name="quixote._c_htmltext", 15 | sources=["src/_c_htmltext.c"]) 16 | 17 | # faster import hook for PTL modules 18 | cimport = Extension(name="quixote.cimport", 19 | sources=["src/cimport.c"]) 20 | 21 | kw = {'name': "Quixote", 22 | 'version': "1.2", 23 | 'description': "A highly Pythonic Web application framework", 24 | 'author': "MEMS Exchange", 25 | 'author_email': "quixote@mems-exchange.org", 26 | 'url': "http://www.mems-exchange.org/software/quixote/", 27 | 'license': "CNRI Open Source License (see LICENSE.txt)", 28 | 29 | 'package_dir': {'quixote': 'quixote'}, 30 | 'packages': ['quixote', 'quixote.demo', 'quixote.form', 31 | 'quixote.form2', 'quixote.server'], 32 | 33 | 'ext_modules': [], 34 | 35 | 'cmdclass': {'build_py': qx_build_py}, 36 | 'tests_require': ['webtest'] 37 | } 38 | 39 | 40 | build_extensions = sys.platform != 'win32' 41 | 42 | if build_extensions: 43 | # The _c_htmltext module requires Python 2.2 features. 44 | if sys.hexversion >= 0x20200a1: 45 | kw['ext_modules'].append(htmltext) 46 | kw['ext_modules'].append(cimport) 47 | 48 | kw['classifiers'] = ['Development Status :: 5 - Production/Stable', 49 | 'Environment :: Web Environment', 50 | 'License :: OSI Approved :: Python License (CNRI Python License)', 51 | 'Intended Audience :: Developers', 52 | 'Operating System :: Unix', 53 | 'Operating System :: Microsoft :: Windows', 54 | 'Operating System :: MacOS :: MacOS X', 55 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 56 | ] 57 | kw['download_url'] = ('http://www.mems-exchange.org/software/files' 58 | '/quixote/Quixote-%s.tar.gz' % kw['version']) 59 | 60 | setup(**kw) 61 | -------------------------------------------------------------------------------- /quixote/qx_distutils.py: -------------------------------------------------------------------------------- 1 | """quixote.qx_distutils 2 | $HeadURL: svn+ssh://svn/repos/trunk/quixote/qx_distutils.py $ 3 | $Id$ 4 | 5 | Provides a version of the Distutils "build_py" command that knows about 6 | PTL files. 7 | 8 | This is installed with Quixote so other Quixote apps can use it in their 9 | setup scripts. 10 | """ 11 | 12 | # created 2001/08/28, Greg Ward (initially written for SPLAT!'s setup.py) 13 | 14 | __revision__ = "$Id$" 15 | 16 | import os, string 17 | from glob import glob 18 | from types import StringType, ListType, TupleType 19 | from distutils.command.build_py import build_py 20 | 21 | # This bites -- way too much code had to be copied from 22 | # distutils/command/build.py just to add an extra file extension! 23 | 24 | class qx_build_py(build_py): 25 | 26 | def find_package_modules(self, package, package_dir): 27 | self.check_package(package, package_dir) 28 | module_files = (glob(os.path.join(package_dir, "*.py")) + 29 | glob(os.path.join(package_dir, "*.ptl"))) 30 | modules = [] 31 | setup_script = os.path.abspath(self.distribution.script_name) 32 | 33 | for f in module_files: 34 | abs_f = os.path.abspath(f) 35 | if abs_f != setup_script: 36 | module = os.path.splitext(os.path.basename(f))[0] 37 | modules.append((package, module, f)) 38 | else: 39 | self.debug_print("excluding %s" % setup_script) 40 | return modules 41 | 42 | def build_module(self, module, module_file, package): 43 | if type(package) is StringType: 44 | package = string.split(package, '.') 45 | elif type(package) not in (ListType, TupleType): 46 | raise TypeError, \ 47 | "'package' must be a string (dot-separated), list, or tuple" 48 | 49 | # Now put the module source file into the "build" area -- this is 50 | # easy, we just copy it somewhere under self.build_lib (the build 51 | # directory for Python source). 52 | outfile = self.get_module_outfile(self.build_lib, package, module) 53 | if module_file.endswith(".ptl"): # XXX hack for PTL 54 | outfile = outfile[0:outfile.rfind('.')] + ".ptl" 55 | dir = os.path.dirname(outfile) 56 | self.mkpath(dir) 57 | return self.copy_file(module_file, outfile, preserve_mode=0) 58 | -------------------------------------------------------------------------------- /quixote/mod_python_handler.py: -------------------------------------------------------------------------------- 1 | """quixote.mod_python_handler 2 | $HeadURL: svn+ssh://svn/repos/trunk/quixote/mod_python_handler.py $ 3 | $Id$ 4 | 5 | mod_python handler for Quixote. See the "mod_python configuration" 6 | section of doc/web-server.txt for details. 7 | """ 8 | 9 | import sys 10 | from mod_python import apache 11 | from quixote import Publisher, enable_ptl 12 | from quixote.config import Config 13 | 14 | class ErrorLog: 15 | def __init__(self, publisher): 16 | self.publisher = publisher 17 | 18 | def write(self, msg): 19 | self.publisher.log(msg) 20 | 21 | def close(self): 22 | pass 23 | 24 | class ModPythonPublisher(Publisher): 25 | def __init__(self, package, config=None): 26 | Publisher.__init__(self, package, config) 27 | self.error_log = self.__error_log = ErrorLog(self) # may be overwritten 28 | self.setup_logs() 29 | self.__apache_request = None 30 | 31 | def log(self, msg): 32 | if self.error_log is self.__error_log: 33 | try: 34 | self.__apache_request.log_error(msg) 35 | except AttributeError: 36 | apache.log_error(msg) 37 | else: 38 | Publisher.log(self, msg) 39 | 40 | def publish_modpython(self, req): 41 | """publish_modpython() -> None 42 | 43 | Entry point from mod_python. 44 | """ 45 | self.__apache_request = req 46 | try: 47 | self.publish(apache.CGIStdin(req), 48 | apache.CGIStdout(req), 49 | sys.stderr, 50 | apache.build_cgi_env(req)) 51 | 52 | return apache.OK 53 | finally: 54 | self.__apache_request = None 55 | 56 | enable_ptl() 57 | 58 | name2publisher = {} 59 | 60 | def handler(req): 61 | opts = req.get_options() 62 | try: 63 | package = opts['quixote-root-namespace'] 64 | except KeyError: 65 | package = None 66 | 67 | try: 68 | configfile = opts['quixote-config-file'] 69 | config = Config() 70 | config.read_file(configfile) 71 | except KeyError: 72 | config = None 73 | 74 | if not package: 75 | return apache.HTTP_INTERNAL_SERVER_ERROR 76 | 77 | pub = name2publisher.get(package) 78 | if pub is None: 79 | pub = ModPythonPublisher(package, config) 80 | name2publisher[package] = pub 81 | return pub.publish_modpython(req) 82 | -------------------------------------------------------------------------------- /doc/ZPL.txt: -------------------------------------------------------------------------------- 1 | Zope Public License (ZPL) Version 2.0 2 | ----------------------------------------------- 3 | 4 | This software is Copyright (c) Zope Corporation (tm) and 5 | Contributors. All rights reserved. 6 | 7 | This license has been certified as open source. It has also 8 | been designated as GPL compatible by the Free Software 9 | Foundation (FSF). 10 | 11 | Redistribution and use in source and binary forms, with or 12 | without modification, are permitted provided that the 13 | following conditions are met: 14 | 15 | 1. Redistributions in source code must retain the above 16 | copyright notice, this list of conditions, and the following 17 | disclaimer. 18 | 19 | 2. Redistributions in binary form must reproduce the above 20 | copyright notice, this list of conditions, and the following 21 | disclaimer in the documentation and/or other materials 22 | provided with the distribution. 23 | 24 | 3. The name Zope Corporation (tm) must not be used to 25 | endorse or promote products derived from this software 26 | without prior written permission from Zope Corporation. 27 | 28 | 4. The right to distribute this software or to use it for 29 | any purpose does not give you the right to use Servicemarks 30 | (sm) or Trademarks (tm) of Zope Corporation. Use of them is 31 | covered in a separate agreement (see 32 | http://www.zope.com/Marks). 33 | 34 | 5. If any files are modified, you must cause the modified 35 | files to carry prominent notices stating that you changed 36 | the files and the date of any change. 37 | 38 | Disclaimer 39 | 40 | THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS'' 41 | AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT 42 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 43 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 44 | NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE 45 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 46 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 47 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 48 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 49 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 50 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 51 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 52 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 53 | DAMAGE. 54 | 55 | 56 | This software consists of contributions made by Zope 57 | Corporation and many individuals on behalf of Zope 58 | Corporation. Specific attributions are listed in the 59 | accompanying credits file. 60 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | ACKS 2 | CHANGES 3 | LICENSE 4 | MANIFEST 5 | MANIFEST.in 6 | README 7 | TODO 8 | __init__.py 9 | _py_htmltext.py 10 | config.py 11 | errors.py 12 | fcgi.py 13 | html.py 14 | http_request.py 15 | http_response.py 16 | mod_python_handler.py 17 | ptl_compile.py 18 | ptl_import.py 19 | ptlc_dump.py 20 | publish.py 21 | qx_distutils.py 22 | sendmail.py 23 | session.py 24 | setup.py 25 | upload.py 26 | util.py 27 | ./__init__.py 28 | ./_py_htmltext.py 29 | ./config.py 30 | ./errors.py 31 | ./fcgi.py 32 | ./html.py 33 | ./http_request.py 34 | ./http_response.py 35 | ./mod_python_handler.py 36 | ./ptl_compile.py 37 | ./ptl_import.py 38 | ./ptlc_dump.py 39 | ./publish.py 40 | ./qx_distutils.py 41 | ./sendmail.py 42 | ./session.py 43 | ./upload.py 44 | ./util.py 45 | ./demo/__init__.py 46 | ./demo/demo_scgi.py 47 | ./demo/forms.ptl 48 | ./demo/integer_ui.py 49 | ./demo/pages.ptl 50 | ./demo/run_cgi.py 51 | ./demo/session.ptl 52 | ./demo/widgets.ptl 53 | ./form/__init__.py 54 | ./form/form.py 55 | ./form/widget.py 56 | ./form2/__init__.py 57 | ./form2/compatibility.py 58 | ./form2/css.py 59 | ./form2/form.py 60 | ./form2/widget.py 61 | ./server/__init__.py 62 | ./server/medusa_http.py 63 | ./server/twisted_http.py 64 | demo/__init__.py 65 | demo/demo.cgi 66 | demo/demo.conf 67 | demo/demo_scgi.py 68 | demo/demo_scgi.sh 69 | demo/forms.ptl 70 | demo/integer_ui.py 71 | demo/pages.ptl 72 | demo/run_cgi.py 73 | demo/session.ptl 74 | demo/session_demo.cgi 75 | demo/upload.cgi 76 | demo/widgets.ptl 77 | doc/INSTALL.html 78 | doc/INSTALL.txt 79 | doc/Makefile 80 | doc/PTL.html 81 | doc/PTL.txt 82 | doc/ZPL.txt 83 | doc/default.css 84 | doc/demo.html 85 | doc/demo.txt 86 | doc/form2conversion.html 87 | doc/form2conversion.txt 88 | doc/multi-threaded.html 89 | doc/multi-threaded.txt 90 | doc/programming.html 91 | doc/programming.txt 92 | doc/session-mgmt.html 93 | doc/session-mgmt.txt 94 | doc/static-files.html 95 | doc/static-files.txt 96 | doc/upgrading.html 97 | doc/upgrading.txt 98 | doc/upload.html 99 | doc/upload.txt 100 | doc/web-server.html 101 | doc/web-server.txt 102 | doc/web-services.html 103 | doc/web-services.txt 104 | doc/widgets.html 105 | doc/widgets.txt 106 | form/__init__.py 107 | form/form.py 108 | form/widget.py 109 | form2/__init__.py 110 | form2/compatibility.py 111 | form2/css.py 112 | form2/form.py 113 | form2/widget.py 114 | server/__init__.py 115 | server/medusa_http.py 116 | server/twisted_http.py 117 | src/Makefile 118 | src/_c_htmltext.c 119 | src/cimport.c 120 | src/setup.py 121 | test/__init__.py 122 | test/ua_test.py 123 | test/utest_html.py 124 | -------------------------------------------------------------------------------- /doc/INSTALL.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Installing Quixote 8 | 9 | 10 | 11 |
12 |

Installing Quixote

13 |
14 |

Best-case scenario

15 |

If you are using Python 2.2 or later, and have never installed Quixote 16 | with your current version of Python, you're in luck. Just run

17 |
18 | python setup.py install
19 |

and you're done. Proceed to the demo documentation to learn how to get 20 | Quixote working.

21 |

If you're using an older Python, or if you're upgrading from an older 22 | Quixote version, read on.

23 |
24 |
25 |

Upgrading from an older Quixote version

26 |

We strongly recommend that you remove any old Quixote version before 27 | installing a new one. First, find out where your old Quixote 28 | installation is:

29 |
30 | python -c "import os, quixote; print os.path.dirname(quixote.__file__)"
31 |

and then remove away the reported directory. (If the import fails, then 32 | you don't have an existing Quixote installation.)

33 |

Then proceed as above:

34 |
35 | python setup.py install
36 |
37 |
38 |

Using Quixote with Python 2.0 or 2.1

39 |

If you are using Python 2.0 or 2.1 then you need to install the 40 | compiler package from the Python source distribution. The 41 | compiler package is for parsing Python source code and generating 42 | Python bytecode, and the PTL compiler is built on top of it. With 43 | Python 2.0 and 2.1, this package was included in Python's source 44 | distribution, but not installed as part of the standard library.

45 |

Assuming your Python source distribution is in /tmp/Python-2.1.2:

46 |
47 | cd /tmp/Python-2.1.2/Tools/compiler
48 | python setup.py install
49 | 
50 |

(Obviously, you'll have to adjust this to reflect your Python version 51 | and where you kept the source distribution after installing Python.)

52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CNRI OPEN SOURCE LICENSE AGREEMENT 2 | 3 | IMPORTANT: PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY. BY 4 | COPYING, INSTALLING OR OTHERWISE USING QUIXOTE-1.2 SOFTWARE, YOU 5 | ARE DEEMED TO HAVE AGREED TO THE TERMS AND CONDITIONS OF THIS 6 | LICENSE AGREEMENT. 7 | 8 | 1. This LICENSE AGREEMENT is between Corporation for National 9 | Research Initiatives, having an office at 1895 Preston White 10 | Drive, Reston, VA 20191 ("CNRI"), and the Individual or 11 | Organization ("Licensee") copying, installing or otherwise using 12 | Quixote-1.2 software in source or binary form and its associated 13 | documentation ("Quixote-1.2"). 14 | 15 | 2. Subject to the terms and conditions of this License Agreement, 16 | CNRI hereby grants Licensee a nonexclusive, royalty-free, world- 17 | wide license to reproduce, analyze, test, perform and/or display 18 | publicly, prepare derivative works, distribute, and otherwise use 19 | Quixote-1.2 alone or in any derivative version, provided, 20 | however, that CNRI's License Agreement and CNRI's notice of 21 | copyright, i.e., "Copyright (c) 2004 Corporation for National 22 | Research Initiatives; All Rights Reserved" are retained in 23 | Quixote-1.2 alone or in any derivative version prepared by 24 | Licensee. 25 | 26 | 3. In the event Licensee prepares a derivative work that is based on 27 | or incorporates Quixote-1.2 or any part thereof, and wants to 28 | make the derivative work available to others as provided herein, 29 | then Licensee hereby agrees to include in any such work a brief 30 | summary of the changes made to Quixote-1.2. 31 | 32 | 4. CNRI is making Quixote-1.2 available to Licensee on an "AS IS" 33 | basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 34 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO 35 | AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY 36 | OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF QUIXOTE- 37 | 1.2 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 38 | 39 | 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF 40 | QUIXOTE-1.2 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES 41 | OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE 42 | USING QUIXOTE-1.2, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF 43 | THE POSSIBILITY THEREOF. 44 | 45 | 6. This License Agreement will automatically terminate upon a 46 | material breach of its terms and conditions. 47 | 48 | 7. This License Agreement shall be governed by and interpreted in 49 | all respects by the law of the State of Virginia, excluding 50 | Virginia's conflict of law provisions. Nothing in this License 51 | Agreement shall be deemed to create any relationship of agency, 52 | partnership, or joint venture between CNRI and Licensee. This 53 | License Agreement does not grant permission to use CNRI 54 | trademarks or trade name in a trademark sense to endorse or 55 | promote products or services of Licensee, or any third party. 56 | 57 | 8. By copying, installing or otherwise using Quixote-1.2, Licensee 58 | agrees to be bound by the terms and conditions of this License 59 | Agreement. 60 | 61 | -------------------------------------------------------------------------------- /quixote/demo/pages.ptl: -------------------------------------------------------------------------------- 1 | # quixote.demo.pages 2 | # 3 | # Provides miscellaneous pages for the Quixote demo (currently 4 | # just the index page). 5 | 6 | __revision__ = "$Id: pages.ptl 25234 2004-09-30 17:36:19Z nascheme $" 7 | 8 | 9 | def _q_index [html] (request): 10 | print "debug message from the index page" 11 | package_name = str('.').join(__name__.split(str('.'))[:-1]) 12 | module_name = __name__ 13 | module_file = __file__ 14 | """ 15 | 16 | Quixote Demo 17 | 18 |

Hello, world!

19 | 20 |

(This page is generated by the index function for the 21 | %(package_name)s package. This index function is 22 | actually a PTL template, _q_index(), in the 23 | %(module_name)s PTL module. Look in 24 | %(module_file)s to 25 | see the source code for this PTL template.) 26 |

27 | 28 |

To understand what's going on here, be sure to read the 29 | doc/demo.txt file included with Quixote.

30 | 31 |

32 | Here are some other features of this demo: 33 |

    34 |
  • simple: 35 | A Python function that generates a very simple document. 36 |
  • error: 37 | A Python function that raises an exception. 38 |
  • publish_error: 39 | A Python function that raises 40 | a PublishError exception. This exception 41 | will be caught by a _q_exception_handler method. 42 |
  • 12/: 43 | A Python object published through _q_lookup(). 44 |
  • 12/factorial: 45 | A method on a published Python object. 46 |
  • dumpreq: 47 | Print out the contents of the HTTPRequest object. 48 |
  • widgets: 49 | Try out the Quixote widget classes. 50 |
  • form demo: 51 | A Quixote form in action. 52 |
  • srcdir: 53 | A static directory published through Quixote. 54 |
55 |

56 | 57 | 58 | """ % vars() 59 | 60 | def _q_exception_handler [html] (request, exc): 61 | """ 62 | 63 | Quixote Demo 64 | 65 |

Exception Handler

66 |

A _q_exception_handler method, if present, is 67 | called when a PublishError exception is raised. It 68 | can do whatever it likes to provide a friendly page. 69 |

70 |

Here's the exception that was raised:
71 | %s (%s).

72 | 73 | 74 | """ % (repr(exc), str(exc)) 75 | 76 | def dumpreq [html] (request): 77 | """ 78 | 79 | HTTPRequest Object 80 | 81 |

HTTPRequest Object

82 | """ 83 | htmltext(request.dump_html()) 84 | """ 85 | 86 | 87 | """ 88 | -------------------------------------------------------------------------------- /doc/static-files.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Examples of serving static files 8 | 9 | 10 | 11 |
12 |

Examples of serving static files

13 |

The quixote.util module includes classes for making files and 14 | directories available as Quixote resources. Here are some examples.

15 |
16 |

Publishing a Single File

17 |

The StaticFile class makes an individual filesystem file (possibly 18 | a symbolic link) available. You can also specify the MIME type and 19 | encoding of the file; if you don't specify this, the MIME type will be 20 | guessed using the standard Python mimetypes.guess_type() function. 21 | The default action is to not follow symbolic links, but this behaviour 22 | can be changed using the follow_symlinks parameter.

23 |

The following example publishes a file with the URL .../stylesheet_css:

24 |
25 | # 'stylesheet_css' must be in the _q_exports list
26 | _q_exports = [ ..., 'stylesheet_css', ...]
27 | 
28 | stylesheet_css = StaticFile(
29 |         "/htdocs/legacy_app/stylesheet.css",
30 |         follow_symlinks=1, mime_type="text/css")
31 | 
32 |

If you want the URL of the file to have a .css extension, you use 33 | the external to internal name mapping feature of _q_exports. For 34 | example:

35 |
36 | _q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...]
37 | 
38 |
39 |
40 |

Publishing a Directory

41 |

Publishing a directory is similar. The StaticDirectory class 42 | makes a complete filesystem directory available. Again, the default 43 | behaviour is to not follow symlinks. You can also request that the 44 | StaticDirectory object cache information about the files in 45 | memory so that it doesn't try to guess the MIME type on every hit.

46 |

This example publishes the notes/ directory:

47 |
48 | _q_exports = [ ..., 'notes', ...]
49 | 
50 | notes = StaticDirectory("/htdocs/legacy_app/notes")
51 | 
52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /quixote/form2/compatibility.py: -------------------------------------------------------------------------------- 1 | '''$URL: svn+ssh://svn/repos/trunk/quixote/form2/compatibility.py $ 2 | $Id$ 3 | 4 | A Form subclass that provides close to the same API as the old form 5 | class (useful for transitioning existing forms). 6 | ''' 7 | 8 | from quixote.form2 import Form as _Form, Widget, StringWidget, FileWidget, \ 9 | PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ 10 | SingleSelectWidget, SelectWidget, OptionSelectWidget, \ 11 | MultipleSelectWidget, SubmitWidget, HiddenWidget, \ 12 | FloatWidget, IntWidget 13 | from quixote.html import url_quote 14 | 15 | _widget_names = { 16 | "string" : StringWidget, 17 | "file" : FileWidget, 18 | "password" : PasswordWidget, 19 | "text" : TextWidget, 20 | "checkbox" : CheckboxWidget, 21 | "single_select" : SingleSelectWidget, 22 | "radiobuttons" : RadiobuttonsWidget, 23 | "multiple_select" : MultipleSelectWidget, 24 | "submit_button" : SubmitWidget, 25 | "hidden" : HiddenWidget, 26 | "float" : FloatWidget, 27 | "int" : IntWidget, 28 | "option_select" : OptionSelectWidget, 29 | } 30 | 31 | 32 | class Form(_Form): 33 | def __init__(self, *args, **kwargs): 34 | _Form.__init__(self, *args, **kwargs) 35 | self.cancel_url = None 36 | 37 | def add_widget(self, widget_class, name, value=None, 38 | title=None, hint=None, required=0, **kwargs): 39 | try: 40 | widget_class = _widget_names[widget_class] 41 | except KeyError: 42 | pass 43 | self.add(widget_class, name, value=value, title=title, hint=hint, 44 | required=required, **kwargs) 45 | 46 | def add_submit_button(self, name, value): 47 | self.add_submit(name, value) 48 | 49 | def add_cancel_button(self, caption, url): 50 | self.add_submit("cancel", caption) 51 | self.cancel_url = url 52 | 53 | def get_action_url(self, request): 54 | action_url = url_quote(request.get_path()) 55 | query = request.get_environ("QUERY_STRING") 56 | if query: 57 | action_url += "?" + query 58 | return action_url 59 | 60 | def render(self, request, action_url=None): 61 | if action_url: 62 | self.action_url = action_url 63 | return _Form.render(self) 64 | 65 | def process(self, request): 66 | values = {} 67 | for name, widget in self._names.items(): 68 | values[name] = widget.parse(request) 69 | return values 70 | 71 | def action(self, request, submit, values): 72 | raise NotImplementedError, "sub-classes must implement 'action()'" 73 | 74 | def handle(self, request): 75 | """handle(request : HTTPRequest) -> string 76 | 77 | Master method for handling forms. It should be called after 78 | initializing a form. Controls form action based on a request. You 79 | probably should override 'process' and 'action' instead of 80 | overriding this method. 81 | """ 82 | if not self.is_submitted(): 83 | return self.render(request, self.action_url) 84 | submit = self.get_submit() 85 | if submit == "cancel": 86 | return request.redirect(self.cancel_url) 87 | values = self.process(request) 88 | if submit == True: 89 | # The form was submitted by an unregistered submit button, assume 90 | # that the submission was required to update the layout of the form. 91 | self.clear_errors() 92 | return self.render(request, self.action_url) 93 | 94 | if self.has_errors(): 95 | return self.render(request, self.action_url) 96 | else: 97 | return self.action(request, submit, values) 98 | -------------------------------------------------------------------------------- /quixote/demo/forms.ptl: -------------------------------------------------------------------------------- 1 | # quixote.demo.forms 2 | # 3 | # Demonstrate the Quixote form class. 4 | 5 | __revision__ = "$Id: forms.ptl 25234 2004-09-30 17:36:19Z nascheme $" 6 | 7 | 8 | import time 9 | from quixote.form import Form 10 | 11 | class Topping: 12 | def __init__(self, name, cost): 13 | self.name = name 14 | self.cost = cost # in cents 15 | 16 | def __str__(self): 17 | return "%s: $%.2f" % (self.name, self.cost/100.) 18 | 19 | def __repr__(self): 20 | return "<%s at %08x: %s>" % (self.__class__.__name__, 21 | id(self), self) 22 | 23 | 24 | TOPPINGS = [Topping('cheese', 50), 25 | Topping('pepperoni', 110), 26 | Topping('green peppers', 75), 27 | Topping('mushrooms', 90), 28 | Topping('sausage', 100), 29 | Topping('anchovies', 30), 30 | Topping('onions', 25)] 31 | 32 | class FormDemo(Form): 33 | def __init__(self): 34 | # build form 35 | Form.__init__(self) 36 | self.add_widget("string", "name", title="Your Name", 37 | size=20, required=1) 38 | self.add_widget("password", "password", title="Password", 39 | size=20, maxlength=20, required=1) 40 | self.add_widget("checkbox", "confirm", 41 | title="Are you sure?") 42 | self.add_widget("radiobuttons", "color", title="Eye color", 43 | allowed_values=['green', 'blue', 'brown', 'other']) 44 | self.add_widget("single_select", "size", title="Size of pizza", 45 | value='medium', 46 | allowed_values=['tiny', 'small', 'medium', 'large', 47 | 'enormous'], 48 | descriptions=['Tiny (4")', 'Small (6")', 'Medium (10")', 49 | 'Large (14")', 'Enormous (18")'], 50 | size=1) 51 | # select widgets can use any type of object, no just strings 52 | self.add_widget("multiple_select", "toppings", title="Pizza Toppings", 53 | value=TOPPINGS[0], 54 | allowed_values=TOPPINGS, 55 | size=5) 56 | self.add_widget('hidden', 'time', value=time.time()) 57 | self.add_submit_button("go", "Go!") 58 | 59 | 60 | def render [html] (self, request, action_url): 61 | """ 62 | 63 | Quixote Form Demo 64 | 65 |

Quixote Form Demo

66 | """ 67 | Form.render(self, request, action_url) 68 | """ 69 | 70 | 71 | """ 72 | 73 | 74 | def process(self, request): 75 | # check data 76 | form_data = Form.process(self, request) 77 | if not form_data["name"]: 78 | self.error["name"] = "You must provide your name." 79 | if not form_data["password"]: 80 | self.error["password"] = "You must provide a password." 81 | return form_data 82 | 83 | 84 | def action [html] (self, request, submit_button, form_data): 85 | # The data has been submitted and verified. Do something interesting 86 | # with it (save it in DB, send email, etc.). We'll just display it. 87 | """ 88 | 89 | Quixote Form Demo 90 | 91 |

Form data:

92 | 93 | 94 | 95 | 96 | 97 | 98 | """ 99 | for name, value in form_data.items(): 100 | '' 101 | ' ' % name 102 | ' ' % type(value).__name__ 103 | if value is None: 104 | value = "no value" 105 | ' ' % value 106 | '' 107 | """ 108 |
NameTypeValue
%s%s%s
109 | 110 | 111 | """ 112 | 113 | def form_demo(request): 114 | return FormDemo().handle(request) 115 | -------------------------------------------------------------------------------- /quixote/demo/widgets.ptl: -------------------------------------------------------------------------------- 1 | # quixote.demo.widgets 2 | # 3 | # Demonstrate the Quixote widget classes. 4 | 5 | __revision__ = "$Id: widgets.ptl 23532 2004-02-20 22:38:57Z dbinger $" 6 | 7 | 8 | import time 9 | from quixote.form import widget 10 | 11 | 12 | def widgets(request): 13 | 14 | # Whether we are generating or processing the form with these 15 | # widgets, we need all the widget objects -- so create them now. 16 | widgets = {} 17 | widgets['name'] = widget.StringWidget('name', size=20) 18 | widgets['password'] = widget.PasswordWidget( 19 | 'password', size=20, maxlength=20) 20 | widgets['confirm'] = widget.CheckboxWidget('confirm') 21 | widgets['colour'] = widget.RadiobuttonsWidget( 22 | 'colour', allowed_values=['green', 'blue', 'brown', 'other']) 23 | widgets['size'] = widget.SingleSelectWidget( 24 | 'size', value='medium', 25 | allowed_values=['tiny', 'small', 'medium', 'large', 'enormous'], 26 | descriptions=['Tiny (4")', 'Small (6")', 'Medium (10")', 27 | 'Large (14")', 'Enormous (18")']) 28 | widgets['toppings'] = widget.MultipleSelectWidget( 29 | 'toppings', value=['cheese'], 30 | allowed_values=['cheese', 'pepperoni', 'green peppers', 'mushrooms', 31 | 'sausage', 'anchovies', 'onions'], 32 | size=5) 33 | widgets['time'] = widget.HiddenWidget('time', value=time.time()) 34 | 35 | if request.form: 36 | # If we have some form data, then we're being invoked to process 37 | # the form; call process_widgets() to do the real work. We only 38 | # handle it in this page to conserve urls: the "widget" url both 39 | # generates the form and processes it, and behaves very 40 | # differently depending on whether there are form variables 41 | # present when it is invoked. 42 | return process_widgets(request, widgets) 43 | else: 44 | # No form data, so generate the form from scratch. When the 45 | # user submits it, we'll come back to this page, but 46 | # request.form won't be empty that time -- so we'll call 47 | # process_widgets() instead. 48 | return render_widgets(request, widgets) 49 | 50 | 51 | def render_widgets [html] (request, widgets): 52 | """\ 53 | 54 | Quixote Widget Demo 55 | 56 |

Quixote Widget Demo

57 | """ 58 | 59 | """\ 60 |
61 | 62 | """ 63 | row_fmt = '''\ 64 | 65 | 66 | 67 | 68 | ''' 69 | row_fmt % ("Your name", widgets['name'].render(request)) 70 | row_fmt % ("Password", widgets['password'].render(request)) 71 | row_fmt % ("Are you sure?", widgets['confirm'].render(request)) 72 | row_fmt % ("Eye colour", widgets['colour'].render(request)) 73 | 74 | '''\ 75 | 76 | 77 | 78 | 79 | 80 | 81 | ''' % (widgets['size'].render(request), 82 | widgets['toppings'].render(request)) 83 | 84 | widgets['time'].render(request) 85 | 86 | '
%s%s
Select a
size of pizza
%sAnd some
pizza toppings
%s
\n' 87 | widget.SubmitButtonWidget(value="Submit").render(request) 88 | '''\ 89 |
90 | 91 | 92 | ''' 93 | 94 | def process_widgets [html] (request, widgets): 95 | """\ 96 | 97 | Quixote Widget Demo 98 | 99 |

You entered the following values:

100 | 101 | """ 102 | 103 | row_fmt = ' \n' 104 | fallback = 'nothing' 105 | row_fmt % ("name", 106 | widgets['name'].parse(request) or fallback) 107 | row_fmt % ("password", 108 | widgets['password'].parse(request) or fallback) 109 | row_fmt % ("confirmation", 110 | widgets['confirm'].parse(request)) 111 | row_fmt % ("eye colour", 112 | widgets['colour'].parse(request) or fallback) 113 | row_fmt % ("pizza size", 114 | widgets['size'].parse(request) or fallback) 115 | toppings = widgets['toppings'].parse(request) 116 | row_fmt % ("pizza toppings", 117 | toppings and (", ".join(toppings)) or fallback) 118 | 119 | '
%s%s
\n' 120 | 121 | form_time = float(widgets['time'].parse(request)) 122 | now = time.time() 123 | ("

It took you %.1f sec to fill out and submit the form

\n" 124 | % (now - form_time)) 125 | 126 | """\ 127 | 128 | 129 | """ 130 | -------------------------------------------------------------------------------- /quixote/demo/session.ptl: -------------------------------------------------------------------------------- 1 | # quixote.demo.session 2 | # 3 | # Application code for the Quixote session management demo. 4 | # Driver script is session_demo.cgi. 5 | 6 | __revision__ = "$Id: session.ptl 23532 2004-02-20 22:38:57Z dbinger $" 7 | 8 | from quixote import get_session_manager 9 | from quixote.errors import QueryError 10 | 11 | _q_exports = ['login', 'logout'] 12 | 13 | 14 | # Typical stuff for any Quixote app. 15 | 16 | def page_header [html] (title): 17 | '''\ 18 | 19 | %s 20 | 21 |

%s

22 | ''' % (title, title) 23 | 24 | def page_footer [html] (): 25 | '''\ 26 | 27 | 28 | ''' 29 | 30 | 31 | # We include the login form on two separate pages, so it's been factored 32 | # out to a separate template. 33 | 34 | def login_form [html] (): 35 | ''' 36 |
37 | 38 | 39 |
40 | ''' 41 | 42 | 43 | def _q_index [html] (request): 44 | page_header("Quixote Session Management Demo") 45 | 46 | session = request.session 47 | 48 | # All Quixote sessions have the ability to track the user's identity 49 | # in session.user. In this simple application, session.user is just 50 | # a string which the user enters directly into this form. In the 51 | # real world, you would of course use a more sophisticated form of 52 | # authentication (eg. enter a password over an SSL connection), and 53 | # session.user might be an object with information about the user 54 | # (their email address, password hash, preferences, etc.). 55 | 56 | if session.user is None: 57 | ''' 58 |

You haven\'t introduced yourself yet.
59 | Please tell me your name: 60 | ''' 61 | login_form() 62 | else: 63 | '

Hello, %s. Good to see you again.

\n' % session.user 64 | 65 | ''' 66 | You can now: 67 | \n' 73 | 74 | # The other piece of information we track here is the number of 75 | # requests made in each session; report that information for the 76 | # current session here. 77 | """\ 78 |

Your session is %s
79 | You have made %d request(s) (including this one) in this session.

80 | """ % (repr(session), session.num_requests) 81 | 82 | # The session manager is the collection of all sessions managed by 83 | # the current publisher, ie. in this process. Poking around in the 84 | # session manager is not something you do often, but it's really 85 | # handy for debugging/site administration. 86 | mgr = get_session_manager() 87 | session_ids = mgr.keys() 88 | ''' 89 |

The current session manager is %s
90 | It has %d session(s) in it right now:

91 | 92 | 93 | ''' % (repr(mgr), len(session_ids)) 94 | for sess_id in session_ids: 95 | sess = mgr[sess_id] 96 | (' \n' 97 | % (sess.id, 98 | sess.user and sess.user or "none", 99 | sess.num_requests)) 100 | '
session idusernum requests
%s%s%d
\n' 101 | 102 | page_footer() 103 | 104 | 105 | # The login() template has two purposes: to display a page with just a 106 | # login form, and to process the login form submitted either from the 107 | # index page or from login() itself. This is a fairly common idiom in 108 | # Quixote (just as it's a fairly common idiom with CGI scripts -- it's 109 | # just cleaner with Quixote). 110 | 111 | def login [html] (request): 112 | page_header("Quixote Session Demo: Login") 113 | session = request.session 114 | 115 | # We seem to be processing the login form. 116 | if request.form: 117 | user = request.form.get("name") 118 | if not user: 119 | raise QueryError("no user name supplied") 120 | 121 | session.user = user 122 | 123 | '

Welcome, %s! Thank you for logging in.

\n' % user 124 | 'back to start\n' 125 | 126 | # No form data to process, so generate the login form instead. When 127 | # the user submits it, we'll return to this template and take the 128 | # above branch. 129 | else: 130 | '

Please enter your name here:

\n' 131 | login_form() 132 | 133 | page_footer() 134 | 135 | 136 | # logout() just expires the current session, ie. removes it from the 137 | # session manager and instructs the client to forget about the session 138 | # cookie. The only code necessary is the call to 139 | # SessionManager.expire_session() -- the rest is just user interface. 140 | 141 | def logout [html] (request): 142 | page_header("Quixote Session Demo: Logout") 143 | session = request.session 144 | if session.user: 145 | '

Goodbye, %s. See you around.

\n' % session.user 146 | 147 | get_session_manager().expire_session(request) 148 | 149 | '

Your session has been expired.

\n' 150 | '

start over

\n' 151 | page_footer() 152 | -------------------------------------------------------------------------------- /quixote/ptl_import.py: -------------------------------------------------------------------------------- 1 | """quixote.ptl_import 2 | $HeadURL: svn+ssh://svn/repos/trunk/quixote/ptl_import.py $ 3 | $Id$ 4 | 5 | Import hooks; when installed, these hooks allow importing .ptl files 6 | as if they were Python modules. 7 | 8 | Note: there's some unpleasant incompatibility between ZODB's import 9 | trickery and the import hooks here. Bottom line: if you're using ZODB, 10 | import it *before* installing the Quixote/PTL import hooks. 11 | """ 12 | 13 | __revision__ = "$Id$" 14 | 15 | 16 | import sys 17 | import os.path 18 | import imp, ihooks, new 19 | import marshal 20 | import stat 21 | import fcntl 22 | import errno 23 | import __builtin__ 24 | 25 | from ptl_compile import compile_template, PTL_EXT, PTLC_EXT, PTLC_MAGIC 26 | 27 | assert sys.hexversion >= 0x20000b1, "need Python 2.0b1 or later" 28 | 29 | def _exec_module_code(code, name, filename): 30 | if sys.modules.has_key(name): 31 | mod = sys.modules[name] # necessary for reload() 32 | else: 33 | mod = new.module(name) 34 | sys.modules[name] = mod 35 | mod.__name__ = name 36 | mod.__file__ = filename 37 | exec code in mod.__dict__ 38 | return mod 39 | 40 | def _load_ptlc(name, filename, file=None): 41 | if not file: 42 | try: 43 | file = open(filename, "rb") 44 | except IOError: 45 | return None 46 | path, ext = os.path.splitext(filename) 47 | ptl_filename = path + PTL_EXT 48 | magic = file.read(len(PTLC_MAGIC)) 49 | if magic != PTLC_MAGIC: 50 | return _load_ptl(name, ptl_filename) 51 | ptlc_mtime = marshal.load(file) 52 | try: 53 | mtime = os.stat(ptl_filename)[stat.ST_MTIME] 54 | except OSError: 55 | mtime = ptlc_mtime 56 | if mtime > ptlc_mtime: 57 | return _load_ptl(name, ptl_filename) 58 | code = marshal.load(file) 59 | return _exec_module_code(code, name, filename) 60 | 61 | def _load_ptl(name, filename, file=None): 62 | if not file: 63 | try: 64 | file = open(filename, "rb") 65 | except IOError: 66 | return None 67 | path, ext = os.path.splitext(filename) 68 | ptlc_filename = path + PTLC_EXT 69 | try: 70 | output_fd = os.open(ptlc_filename, os.O_WRONLY | os.O_CREAT, 0644) 71 | try: 72 | fcntl.flock(output_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 73 | except IOError, e: 74 | if e.errno == errno.EWOULDBLOCK: 75 | fcntl.flock(output_fd, fcntl.LOCK_EX) 76 | os.close(output_fd) 77 | return _load_ptlc(name, ptlc_filename) 78 | raise 79 | output = os.fdopen(output_fd, 'wb') 80 | except (OSError, IOError), e: 81 | output = None 82 | try: 83 | code = compile_template(file, filename, output) 84 | except: 85 | # don't leave a corrupt .ptlc file around 86 | if output: 87 | output.close() 88 | os.unlink(ptlc_filename) 89 | raise 90 | else: 91 | if output: 92 | output.close() 93 | return _exec_module_code(code, name, filename) 94 | 95 | 96 | # Constant used to signal a PTL files 97 | PTLC_FILE = 128 98 | PTL_FILE = 129 99 | 100 | class PTLHooks(ihooks.Hooks): 101 | 102 | def get_suffixes(self): 103 | # add our suffixes 104 | L = imp.get_suffixes() 105 | return L + [(PTLC_EXT, 'rb', PTLC_FILE), (PTL_EXT, 'r', PTL_FILE)] 106 | 107 | class PTLLoader(ihooks.ModuleLoader): 108 | 109 | def load_module(self, name, stuff): 110 | file, filename, info = stuff 111 | (suff, mode, type) = info 112 | 113 | # If it's a PTL file, load it specially. 114 | if type == PTLC_FILE: 115 | return _load_ptlc(name, filename, file) 116 | 117 | elif type == PTL_FILE: 118 | return _load_ptl(name, filename, file) 119 | 120 | else: 121 | # Otherwise, use the default handler for loading 122 | return ihooks.ModuleLoader.load_module( self, name, stuff) 123 | 124 | try: 125 | import cimport 126 | except ImportError: 127 | cimport = None 128 | 129 | class cModuleImporter(ihooks.ModuleImporter): 130 | def __init__(self, loader=None): 131 | self.loader = loader or ihooks.ModuleLoader() 132 | cimport.set_loader(self.find_import_module) 133 | 134 | def find_import_module(self, fullname, subname, path): 135 | stuff = self.loader.find_module(subname, path) 136 | if not stuff: 137 | return None 138 | return self.loader.load_module(fullname, stuff) 139 | 140 | def install(self): 141 | self.save_import_module = __builtin__.__import__ 142 | self.save_reload = __builtin__.reload 143 | if not hasattr(__builtin__, 'unload'): 144 | __builtin__.unload = None 145 | self.save_unload = __builtin__.unload 146 | __builtin__.__import__ = cimport.import_module 147 | __builtin__.reload = cimport.reload_module 148 | __builtin__.unload = self.unload 149 | 150 | _installed = 0 151 | 152 | def install(): 153 | global _installed 154 | if not _installed: 155 | hooks = PTLHooks() 156 | loader = PTLLoader(hooks) 157 | if cimport is not None: 158 | importer = cModuleImporter(loader) 159 | else: 160 | importer = ihooks.ModuleImporter(loader) 161 | ihooks.install(importer) 162 | _installed = 1 163 | 164 | 165 | if __name__ == '__main__': 166 | import ZODB 167 | install() 168 | -------------------------------------------------------------------------------- /quixote/server/medusa_http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """quixote.server.medusa_http 4 | 5 | An HTTP handler for Medusa that publishes a Quixote application. 6 | """ 7 | 8 | __revision__ = "$Id$" 9 | 10 | # A simple HTTP server, using Medusa, that publishes a Quixote application. 11 | 12 | import sys 13 | import asyncore, rfc822, socket, urllib 14 | from StringIO import StringIO 15 | from medusa import http_server, xmlrpc_handler 16 | from quixote.http_response import Stream 17 | from quixote.publish import Publisher 18 | 19 | 20 | class StreamProducer: 21 | def __init__(self, stream): 22 | self.iterator = iter(stream) 23 | 24 | def more(self): 25 | try: 26 | return self.iterator.next() 27 | except StopIteration: 28 | return '' 29 | 30 | 31 | class QuixoteHandler: 32 | def __init__(self, publisher, server_name, server): 33 | """QuixoteHandler(publisher:Publisher, server_name:string, 34 | server:medusa.http_server.http_server) 35 | 36 | Publish the specified Quixote publisher. 'server_name' will 37 | be passed as the SERVER_NAME environment variable. 38 | """ 39 | self.publisher = publisher 40 | self.server_name = server_name 41 | self.server = server 42 | 43 | def match(self, request): 44 | # Always match, since this is the only handler there is. 45 | return 1 46 | 47 | def handle_request(self, request): 48 | msg = rfc822.Message(StringIO('\n'.join(request.header))) 49 | length = int(msg.get('Content-Length', '0')) 50 | if length: 51 | request.collector = xmlrpc_handler.collector(self, request) 52 | else: 53 | self.continue_request('', request) 54 | 55 | def continue_request(self, data, request): 56 | msg = rfc822.Message(StringIO('\n'.join(request.header))) 57 | remote_addr, remote_port = request.channel.addr 58 | if '#' in request.uri: 59 | # MSIE is buggy and sometimes includes fragments in URLs 60 | [request.uri, fragment] = request.uri.split('#', 1) 61 | if '?' in request.uri: 62 | [path, query_string] = request.uri.split('?', 1) 63 | else: 64 | path = request.uri 65 | query_string = '' 66 | 67 | path = urllib.unquote(path) 68 | server_port = str(self.server.port) 69 | http_host = msg.get("Host") 70 | if http_host: 71 | if ":" in http_host: 72 | server_name, server_port = http_host.split(":", 1) 73 | else: 74 | server_name = http_host 75 | else: 76 | server_name = (self.server.ip or 77 | socket.gethostbyaddr(socket.gethostname())[0]) 78 | 79 | environ = {'REQUEST_METHOD': request.command, 80 | 'ACCEPT_ENCODING': msg.get('Accept-encoding', ''), 81 | 'CONTENT_TYPE': msg.get('Content-type', ''), 82 | 'CONTENT_LENGTH': len(data), 83 | "GATEWAY_INTERFACE": "CGI/1.1", 84 | 'PATH_INFO': path, 85 | 'QUERY_STRING': query_string, 86 | 'REMOTE_ADDR': remote_addr, 87 | 'REMOTE_PORT': str(remote_port), 88 | 'REQUEST_URI': request.uri, 89 | 'SCRIPT_NAME': '', 90 | "SCRIPT_FILENAME": '', 91 | 'SERVER_NAME': server_name, 92 | 'SERVER_PORT': server_port, 93 | 'SERVER_PROTOCOL': 'HTTP/1.1', 94 | 'SERVER_SOFTWARE': self.server_name, 95 | } 96 | for title, header in msg.items(): 97 | envname = 'HTTP_' + title.replace('-', '_').upper() 98 | environ[envname] = header 99 | 100 | stdin = StringIO(data) 101 | qreq = self.publisher.create_request(stdin, environ) 102 | output = self.publisher.process_request(qreq, environ) 103 | 104 | qresponse = qreq.response 105 | if output: 106 | qresponse.set_body(output) 107 | 108 | # Copy headers from Quixote's HTTP response 109 | for name, value in qresponse.generate_headers(): 110 | # XXX Medusa's HTTP request is buggy, and only allows unique 111 | # headers. 112 | request[name] = value 113 | 114 | request.response(qresponse.status_code) 115 | 116 | # XXX should we set a default Last-Modified time? 117 | if qresponse.body is not None: 118 | if isinstance(qresponse.body, Stream): 119 | request.push(StreamProducer(qresponse.body)) 120 | else: 121 | request.push(qresponse.body) 122 | 123 | request.done() 124 | 125 | def main(): 126 | from quixote import enable_ptl 127 | enable_ptl() 128 | 129 | if len(sys.argv) == 2: 130 | port = int(sys.argv[1]) 131 | else: 132 | port = 8080 133 | print 'Now serving the Quixote demo on port %d' % port 134 | server = http_server.http_server('', port) 135 | publisher = Publisher('quixote.demo') 136 | 137 | # When initializing the Publisher in your own driver script, 138 | # you'll want to parse a configuration file. 139 | ##publisher.read_config("/full/path/to/demo.conf") 140 | publisher.setup_logs() 141 | dh = QuixoteHandler(publisher, 'Quixote/demo', server) 142 | server.install_handler(dh) 143 | asyncore.loop() 144 | 145 | if __name__ == '__main__': 146 | main() 147 | -------------------------------------------------------------------------------- /doc/web-services.txt: -------------------------------------------------------------------------------- 1 | Implementing Web Services with Quixote 2 | ====================================== 3 | 4 | This document will show you how to implement Web services using 5 | Quixote. 6 | 7 | 8 | An XML-RPC Service 9 | ------------------ 10 | 11 | XML-RPC is the simplest protocol commonly used to expose a Web 12 | service. In XML-RPC, there are a few basic data types such as 13 | integers, floats, strings, and dates, and a few aggregate types such 14 | as arrays and structs. The xmlrpclib module, part of the Python 2.2 15 | standard library and available separately from 16 | http://www.pythonware.com/products/xmlrpc/, converts between Python's 17 | standard data types and the XML-RPC data types. 18 | 19 | ============== ===================== 20 | XML-RPC Type Python Type or Class 21 | -------------- --------------------- 22 | int 23 | float 24 | string 25 | list 26 | dict 27 | xmlrpclib.Boolean 28 | xmlrpclib.Binary 29 | xmlrpclib.DateTime 30 | ============== ===================== 31 | 32 | 33 | Making XML-RPC Calls 34 | -------------------- 35 | 36 | Making an XML-RPC call using xmlrpclib is easy. An XML-RPC server 37 | lives at a particular URL, so the first step is to create an 38 | xmlrpclib.ServerProxy object pointing at that URL. :: 39 | 40 | >>> import xmlrpclib 41 | >>> s = xmlrpclib.ServerProxy( 42 | 'http://www.stuffeddog.com/speller/speller-rpc.cgi') 43 | 44 | Now you can simply make a call to the spell-checking service offered 45 | by this server:: 46 | 47 | >>> s.speller.spellCheck('my speling isnt gud', {}) 48 | [{'word': 'speling', 'suggestions': ['apeling', 'spelding', 49 | 'spelling', 'sperling', 'spewing', 'spiling'], 'location': 4}, 50 | {'word': 'isnt', 'suggestions': [``isn't'', 'ist'], 'location': 12}] 51 | >>> 52 | 53 | This call results in the following XML being sent:: 54 | 55 | 56 | 57 | speller.spellCheck 58 | 59 | 60 | my speling isnt gud 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Writing a Quixote Service 70 | ------------------------- 71 | 72 | In the quixote.util module, Quixote provides a function, 73 | ``xmlrpc(request, func)``, that processes the body of an XML-RPC 74 | request. ``request`` is the HTTPRequest object that Quixote passes to 75 | every function it invokes. ``func`` is a user-supplied function that 76 | receives the name of the XML-RPC method being called and a tuple 77 | containing the method's parameters. If there's a bug in the function 78 | you supply and it raises an exception, the ``xmlrpc()`` function will 79 | catch the exception and return a ``Fault`` to the remote caller. 80 | 81 | Here's an example of implementing a simple XML-RPC handler with a 82 | single method, ``get_time()``, that simply returns the current 83 | time. The first task is to expose a URL for accessing the service. :: 84 | 85 | from quixote.util import xmlrpc 86 | 87 | _q_exports = ['rpc'] 88 | 89 | def rpc (request): 90 | return xmlrpc(request, rpc_process) 91 | 92 | def rpc_process (meth, params): 93 | ... 94 | 95 | When the above code is placed in the __init__.py file for the Python 96 | package corresponding to your Quixote application, it exposes the URL 97 | ``http:///rpc`` as the access point for the XML-RPC service. 98 | 99 | Next, we need to fill in the contents of the ``rpc_process()`` 100 | function:: 101 | 102 | import time 103 | 104 | def rpc_process (meth, params): 105 | if meth == 'get_time': 106 | # params is ignored 107 | now = time.gmtime(time.time()) 108 | return xmlrpclib.DateTime(now) 109 | else: 110 | raise RuntimeError, "Unknown XML-RPC method: %r" % meth 111 | 112 | ``rpc_process()`` receives the method name and the parameters, and its 113 | job is to run the right code for the method, returning a result that 114 | will be marshalled into XML-RPC. The body of ``rpc_process()`` will 115 | therefore usually be an ``if`` statement that checks the name of the 116 | method, and calls another function to do the actual work. In this case, 117 | ``get_time()`` is very simple so the two lines of code it requires are 118 | simply included in the body of ``rpc_process()``. 119 | 120 | If the method name doesn't belong to a supported method, execution 121 | will fall through to the ``else`` clause, which will raise a 122 | RuntimeError exception. Quixote's ``xmlrpc()`` will catch this 123 | exception and report it to the caller as an XML-RPC fault, with the 124 | error code set to 1. 125 | 126 | As you add additional XML-RPC services, the ``if`` statement in 127 | ``rpc_process()`` will grow more branches. You might be tempted to pass 128 | the method name to ``getattr()`` to select a method from a module or 129 | class. That would work, too, and avoids having a continually growing 130 | set of branches, but you should be careful with this and be sure that 131 | there are no private methods that a remote caller could access. I 132 | generally prefer to have the ``if... elif... elif... else`` blocks, for 133 | three reasons: 1) adding another branch isn't much work, 2) it's 134 | explicit about the supported method names, and 3) there won't be any 135 | security holes in doing so. 136 | 137 | An alternative approach is to have a dictionary mapping method names 138 | to the corresponding functions and restrict the legal method names 139 | to the keys of this dictionary:: 140 | 141 | def echo (*params): 142 | # Just returns the parameters it's passed 143 | return params 144 | 145 | def get_time (): 146 | now = time.gmtime(time.time()) 147 | return xmlrpclib.DateTime(now) 148 | 149 | methods = {'echo' : echo, 150 | 'get_time' : get_time} 151 | 152 | def rpc_process (meth, params): 153 | func = methods.get[meth] 154 | if methods.has_key(meth): 155 | # params is ignored 156 | now = time.gmtime(time.time()) 157 | return xmlrpclib.DateTime(now) 158 | else: 159 | raise RuntimeError, "Unknown XML-RPC method: %r" % meth 160 | 161 | This approach works nicely when there are many methods and the 162 | ``if...elif...else`` statement would be unworkably long. 163 | 164 | 165 | $Id: web-services.txt 21603 2003-05-09 19:17:04Z akuchlin $ 166 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Quixote 2 | ======= 3 | 4 | Quixote is yet another framework for developing Web applications in 5 | Python. The design goals were: 6 | 7 | 1) To allow easy development of Web applications where the 8 | emphasis is more on complicated programming logic than 9 | complicated templating. 10 | 11 | 2) To make the templating language as similar to Python as possible, 12 | in both syntax and semantics. The aim is to make as many of the 13 | skills and structural techniques used in writing regular Python 14 | code applicable to Web applications built using Quixote. 15 | 16 | 3) No magic. When it's not obvious what to do in 17 | a certain case, Quixote refuses to guess. 18 | 19 | If you view a web site as a program, and web pages as subroutines, 20 | Quixote just might be the tool for you. If you view a web site as a 21 | graphic design showcase, and each web page as an individual work of art, 22 | Quixote is probably not what you're looking for. 23 | 24 | An additional requirement was that the entire system had to be 25 | implementable in a week or two. The initial version of Quixote was 26 | indeed cranked out in about that time -- thank you, Python! 27 | 28 | We've tried to reuse as much existing code as possible: 29 | 30 | * The HTTPRequest and HTTPResponse classes are distantly 31 | derived from their namesakes in Zope, but we've removed 32 | huge amounts of Zope-specific code. 33 | 34 | * The quixote.fcgi module is derived from Robin Dunn's FastCGI module, 35 | available at 36 | http://alldunn.com/python/#fcgi 37 | 38 | Quixote requires Python 2.1 or greater to run. We only test Quixote 39 | with Python 2.3, but it should still work with 2.1 and 2.2. 40 | 41 | For installation instructions, see the doc/INSTALL.txt file (or 42 | http://www.mems-exchange.org/software/quixote/doc/INSTALL.html). 43 | 44 | If you're switching to a newer version of Quixote from an older 45 | version, please refer to doc/upgrading.txt for explanations of any 46 | backward-incompatible changes. 47 | 48 | 49 | Overview 50 | ======== 51 | 52 | Quixote works by using a Python package to store all the code and HTML 53 | for a Web-based application. There's a simple framework for 54 | publishing code and objects on the Web, and the publishing loop can be 55 | customized by subclassing the Publisher class. You can think of it as 56 | a toolkit to build your own smaller, simpler version of Zope, 57 | specialized for your application. 58 | 59 | An application using Quixote is a Python package containing .py and 60 | .ptl files. 61 | 62 | webapp/ # Root of package 63 | __init__.py 64 | module1.py 65 | module2.py 66 | pages1.ptl 67 | pages2.ptl 68 | 69 | PTL, the Python Template Language, is used to mix HTML with Python code. 70 | More importantly, Python can be used to drive the generation of HTML. 71 | An import hook is defined so that PTL files can be imported just like 72 | Python modules. The basic syntax of PTL is Python's, with a few small 73 | changes: 74 | 75 | def plain [text] barebones_header(title=None, 76 | description=None): 77 | """ 78 | 79 | %s 80 | """ % html_quote(str(title)) 81 | if description: 82 | '' % html_quote(description) 83 | 84 | '' 85 | 86 | See doc/PTL.txt for a detailed explanation of PTL. 87 | 88 | 89 | Quick start 90 | =========== 91 | 92 | For instant gratification, see doc/demo.txt. This explains how to get 93 | the Quixote demo up and running, so you can play with Quixote without 94 | actually having to write any code. 95 | 96 | 97 | Documentation 98 | ============= 99 | 100 | All the documentation is in the doc/ subdirectory, in both text and 101 | HTML. Or you can browse it online from 102 | http://www.mems-exchange.org/software/quixote/doc/ 103 | 104 | Recommended reading: 105 | 106 | demo.txt getting the Quixote demo up and running, and 107 | how the demo works 108 | programming.txt the components of a Quixote application: how 109 | to write your own Quixote apps 110 | PTL.txt the Python Template Language, used by Quixote 111 | apps to generate web pages 112 | web-server.txt how to configure your web server for Quixote 113 | 114 | Optional reading (more advanced or arcane stuff): 115 | 116 | session-mgmt.txt session management: how to track information 117 | across requests 118 | static-files.txt making static files and CGI scripts available 119 | upload.txt how to handle HTTP uploads with Quixote 120 | upgrading.txt info on backward-incompatible changes that may 121 | affect applications written with earlier versions 122 | widgets.txt reference documentation for the Quixote Widget 123 | classes (which underly the form library) 124 | web-services.txt how to write web services using Quixote and 125 | XML-RPC 126 | 127 | 128 | Authors, copyright, and license 129 | =============================== 130 | 131 | Copyright (c) 2000-2003 CNRI. 132 | 133 | Quixote was primarily written by Andrew Kuchling, Neil Schemenauer, and 134 | Greg Ward. 135 | 136 | Overall, Quixote is covered by the CNRI Open Source License Agreement; 137 | see LICENSE for details. 138 | 139 | Portions of Quixote are derived from Zope, and are also covered by the 140 | ZPL (Zope Public License); see ZPL.txt. 141 | 142 | Full acknowledgments are in the ACKS file. 143 | 144 | 145 | Availability, home page, and mailing lists 146 | ========================================== 147 | 148 | The Quixote home page is: 149 | http://www.mems-exchange.org/software/quixote/ 150 | 151 | You'll find the latest stable release there. The current development 152 | code is also available via CVS; for instructions, see 153 | http://www.mems-exchange.org/software/quixote/cvs.html 154 | 155 | Discussion of Quixote occurs on the quixote-users mailing list: 156 | http://mail.mems-exchange.org/mailman/listinfo/quixote-users/ 157 | 158 | To follow development at the most detailed level by seeing every CVS 159 | checkin, join the quixote-checkins mailing list: 160 | http://mail.mems-exchange.org/mailman/listinfo/quixote-checkins/ 161 | 162 | 163 | -- 164 | A.M. Kuchling 165 | Neil Schemenauer 166 | Greg Ward 167 | -------------------------------------------------------------------------------- /quixote/html.py: -------------------------------------------------------------------------------- 1 | """Various functions for dealing with HTML. 2 | $HeadURL: svn+ssh://svn/repos/trunk/quixote/html.py $ 3 | $Id$ 4 | 5 | These functions are fairly simple but it is critical that they be 6 | used correctly. Many security problems are caused by quoting errors 7 | (cross site scripting is one example). The HTML and XML standards on 8 | www.w3c.org and www.xml.com should be studied, especially the sections 9 | on character sets, entities, attribute and values. 10 | 11 | htmltext and htmlescape 12 | ----------------------- 13 | 14 | This type and function are meant to be used with [html] PTL template type. 15 | The htmltext type designates data that does not need to be escaped and the 16 | htmlescape() function calls str() on the argment, escapes the resulting 17 | string and returns a htmltext instance. htmlescape() does nothing to 18 | htmltext instances. 19 | 20 | 21 | html_quote 22 | ---------- 23 | 24 | Use for quoting data that will be used within attribute values or as 25 | element contents (if the [html] template type is not being used). 26 | Examples: 27 | 28 | '%s' % html_quote(title) 29 | '' % html_quote(data) 30 | 'something' % html_quote(url) 31 | 32 | Note that the \" character should be used to surround attribute values. 33 | 34 | 35 | url_quote 36 | --------- 37 | 38 | Use for quoting data to be included as part of a URL, for example: 39 | 40 | input = "foo bar" 41 | ... 42 | '' % url_quote(input) 43 | 44 | Note that URLs are usually used as attribute values and should be quoted 45 | using html_quote. For example: 46 | 47 | url = 'http://example.com/?a=1©=0' 48 | ... 49 | 'do something' % html_quote(url) 50 | 51 | If html_quote is not used, old browsers would treat "©" as an entity 52 | reference and replace it with the copyright character. XML processors should 53 | treat it as an invalid entity reference. 54 | 55 | """ 56 | 57 | __revision__ = "$Id$" 58 | 59 | import urllib 60 | from types import UnicodeType 61 | 62 | try: 63 | # faster C implementation 64 | from quixote._c_htmltext import htmltext, htmlescape, _escape_string, \ 65 | TemplateIO 66 | except ImportError: 67 | from quixote._py_htmltext import htmltext, htmlescape, _escape_string, \ 68 | TemplateIO 69 | 70 | ValuelessAttr = ["valueless_attr"] # magic singleton object 71 | 72 | def htmltag(tag, xml_end=0, css_class=None, **attrs): 73 | """Create a HTML tag. 74 | """ 75 | r = ["<%s" % tag] 76 | if css_class is not None: 77 | attrs['class'] = css_class 78 | for (attr, val) in attrs.items(): 79 | if val is ValuelessAttr: 80 | val = attr 81 | if val is not None: 82 | r.append(' %s="%s"' % (attr, _escape_string(str(val)))) 83 | if xml_end: 84 | r.append(" />") 85 | else: 86 | r.append(">") 87 | return htmltext("".join(r)) 88 | 89 | 90 | def href(url, text, title=None, **attrs): 91 | return (htmltag("a", href=url, title=title, **attrs) + 92 | htmlescape(text) + 93 | htmltext("")) 94 | 95 | 96 | def nl2br(value): 97 | """nl2br(value : any) -> htmltext 98 | 99 | Insert
tags before newline characters. 100 | """ 101 | text = htmlescape(value) 102 | return htmltext(text.s.replace('\n', '
\n')) 103 | 104 | 105 | def url_quote(value, fallback=None): 106 | """url_quote(value : any [, fallback : string]) -> string 107 | 108 | Quotes 'value' for use in a URL; see urllib.quote(). If value is None, 109 | then the behavior depends on the fallback argument. If it is not 110 | supplied then an error is raised. Otherwise, the fallback value is 111 | returned unquoted. 112 | """ 113 | if value is None: 114 | if fallback is None: 115 | raise ValueError, "value is None and no fallback supplied" 116 | else: 117 | return fallback 118 | if isinstance(value, UnicodeType): 119 | value = value.encode('iso-8859-1') 120 | else: 121 | value = str(value) 122 | return urllib.quote(value) 123 | 124 | 125 | # 126 | # The rest of this module is for Quixote applications that were written 127 | # before 'htmltext'. If you are writing a new application, ignore them. 128 | # 129 | 130 | def html_quote(value, fallback=None): 131 | """html_quote(value : any [, fallback : string]) -> str 132 | 133 | Quotes 'value' for use in an HTML page. The special characters &, 134 | <, > are replaced by SGML entities. If value is None, then the 135 | behavior depends on the fallback argument. If it is not supplied 136 | then an error is raised. Otherwise, the fallback value is returned 137 | unquoted. 138 | """ 139 | if value is None: 140 | if fallback is None: 141 | raise ValueError, "value is None and no fallback supplied" 142 | else: 143 | return fallback 144 | elif isinstance(value, UnicodeType): 145 | value = value.encode('iso-8859-1') 146 | else: 147 | value = str(value) 148 | value = value.replace("&", "&") # must be done first 149 | value = value.replace("<", "<") 150 | value = value.replace(">", ">") 151 | value = value.replace('"', """) 152 | return value 153 | 154 | 155 | def value_quote(value): 156 | """Quote HTML attribute values. This function is of marginal 157 | utility since html_quote can be used. 158 | 159 | XHTML 1.0 requires that all values be quoted. weblint claims 160 | that some clients don't understand single quotes. For compatibility 161 | with HTML, XHTML 1.0 requires that ampersands be encoded. 162 | """ 163 | assert value is not None, "can't pass None to value_quote" 164 | value = str(value).replace('&', '&') 165 | value = value.replace('"', '"') 166 | return '"%s"' % value 167 | 168 | 169 | def link(url, text, title=None, name=None, **kwargs): 170 | return render_tag("a", href=url, title=title, name=name, 171 | **kwargs) + str(text) + "" 172 | 173 | 174 | def render_tag(tag, xml_end=0, **attrs): 175 | r = "<%s" % tag 176 | for (attr, val) in attrs.items(): 177 | if val is ValuelessAttr: 178 | r += ' %s="%s"' % (attr, attr) 179 | elif val is not None: 180 | r += " %s=%s" % (attr, value_quote(val)) 181 | if xml_end: 182 | r += " />" 183 | else: 184 | r += ">" 185 | return r 186 | -------------------------------------------------------------------------------- /quixote/errors.py: -------------------------------------------------------------------------------- 1 | """quixote.errors 2 | $HeadURL: svn+ssh://svn/repos/trunk/quixote/errors.py $ 3 | $Id$ 4 | 5 | Exception classes used by Quixote 6 | """ 7 | from quixote.html import htmltext, htmlescape 8 | 9 | __revision__ = "$Id$" 10 | 11 | 12 | class PublishError(Exception): 13 | """PublishError exceptions are raised due to some problem with the 14 | data provided by the client and are raised during the publishing 15 | process. Quixote will abort the current request and return an error 16 | page to the client. 17 | 18 | public_msg should be a user-readable message that reveals no 19 | inner workings of your application; it will always be shown. 20 | 21 | private_msg will only be shown if the config option SECURE_ERRORS is 22 | false; Quixote uses this to give you more detail about why the error 23 | occurred. You might want to use it for similar, application-specific 24 | information. (SECURE_ERRORS should always be true in a production 25 | environment, since these details about the inner workings of your 26 | application could conceivably be useful to attackers.) 27 | 28 | The formatting done by the Quixote versions of these exceptions is 29 | very simple. Applications will probably wish to raise application 30 | specific subclasses which do more sophisticated formatting or provide 31 | a _q_except handler to format the exception. 32 | 33 | """ 34 | 35 | status_code = 400 # bad request 36 | title = "Publishing error" 37 | description = "no description" 38 | 39 | def __init__(self, public_msg=None, private_msg=None): 40 | self.public_msg = public_msg 41 | self.private_msg = private_msg # cleared if SECURE_ERRORS is true 42 | 43 | def __str__(self): 44 | return self.private_msg or self.public_msg or "???" 45 | 46 | def format(self, request): 47 | msg = htmlescape(self.title) 48 | if not isinstance(self.title, htmltext): 49 | msg = str(msg) # for backwards compatibility 50 | if self.public_msg: 51 | msg = msg + ": " + self.public_msg 52 | if self.private_msg: 53 | msg = msg + ": " + self.private_msg 54 | return msg 55 | 56 | 57 | class TraversalError(PublishError): 58 | """ 59 | Raised when a client attempts to access a resource that does not 60 | exist or is otherwise unavailable to them (eg. a Python function 61 | not listed in its module's _q_exports list). 62 | 63 | path should be the path to the requested resource; if not 64 | supplied, the current request object will be fetched and its 65 | get_path() method called. 66 | """ 67 | 68 | status_code = 404 # not found 69 | title = "Page not found" 70 | description = ("The requested link does not exist on this site. If " 71 | "you arrived here by following a link from an external " 72 | "page, please inform that page's maintainer.") 73 | 74 | def __init__(self, public_msg=None, private_msg=None, path=None): 75 | PublishError.__init__(self, public_msg, private_msg) 76 | if path is None: 77 | import quixote 78 | path = quixote.get_request().get_path() 79 | self.path = path 80 | 81 | def format(self, request): 82 | msg = htmlescape(self.title) + ": " + self.path 83 | if not isinstance(self.title, htmltext): 84 | msg = str(msg) # for backwards compatibility 85 | if self.public_msg: 86 | msg = msg + ": " + self.public_msg 87 | if self.private_msg: 88 | msg = msg + ": " + self.private_msg 89 | return msg 90 | 91 | class RequestError(PublishError): 92 | """ 93 | Raised when Quixote is unable to parse an HTTP request (or its CGI 94 | representation). This is a lower-level error than QueryError -- it 95 | either means that Quixote is not smart enough to handle the request 96 | being passed to it, or the user-agent is broken and/or malicious. 97 | """ 98 | status_code = 400 99 | title = "Invalid request" 100 | description = "Unable to parse HTTP request." 101 | 102 | 103 | class QueryError(PublishError): 104 | """Should be raised if bad data was provided in the query part of a 105 | URL or in the content of a POST request. What constitutes bad data is 106 | solely application dependent (eg: letters in a form field when the 107 | application expects a number). 108 | """ 109 | 110 | status_code = 400 111 | title = "Invalid query" 112 | description = ("An error occurred while handling your request. The " 113 | "query data provided as part of the request is invalid.") 114 | 115 | 116 | 117 | class AccessError(PublishError): 118 | """Should be raised if the client does not have access to the 119 | requested resource. Usually applications will raise this error from 120 | an _q_access method. 121 | """ 122 | 123 | status_code = 403 124 | title = "Access denied" 125 | description = ("An error occurred while handling your request. " 126 | "Access to the requested resource was not permitted.") 127 | 128 | 129 | 130 | class SessionError(PublishError): 131 | """Raised when a session cookie has an invalid session ID. This 132 | could be either a broken/malicious client or an expired session. 133 | """ 134 | 135 | status_code = 400 136 | title = "Expired or invalid session" 137 | description = ("Your session is invalid or has expired. " 138 | "Please reload this page to start a new session.") 139 | 140 | def __init__(self, public_msg=None, private_msg=None, session_id=None): 141 | PublishError.__init__(self, public_msg, private_msg) 142 | self.session_id = session_id 143 | 144 | def format(self, request): 145 | from quixote import get_session_manager 146 | get_session_manager().revoke_session_cookie(request) 147 | msg = PublishError.format(self, request) 148 | if self.session_id: 149 | msg = msg + ": " + self.session_id 150 | return msg 151 | 152 | 153 | def default_exception_handler(request, exc): 154 | """(request : HTTPRequest, exc : PublishError) -> string 155 | 156 | Format a PublishError exception as a web page. This is the default 157 | handler called if no '_q_exception_handler' function was found while 158 | traversing the path. 159 | """ 160 | return htmltext("""\ 161 | 163 | 164 | Error: %s 165 | 166 |

%s

167 |

%s

168 | 169 | 170 | """) % (exc.title, exc.description, exc.format(request)) 171 | -------------------------------------------------------------------------------- /quixote/demo/session_demo.cgi: -------------------------------------------------------------------------------- 1 | #!/www/python/bin/python 2 | 3 | # Demonstrate Quixote session management, along with the application 4 | # code in session.ptl (aka quixote.demo.session). 5 | 6 | __revision__ = "$Id: session_demo.cgi 21182 2003-03-17 21:46:52Z gward $" 7 | 8 | import os 9 | from stat import ST_MTIME 10 | from time import time 11 | from cPickle import load, dump 12 | from quixote import enable_ptl 13 | from quixote.session import Session, SessionManager 14 | from quixote.publish import SessionPublisher 15 | 16 | class DemoSession (Session): 17 | """ 18 | Session class that tracks the number of requests made within a 19 | session. 20 | """ 21 | 22 | def __init__ (self, request, id): 23 | Session.__init__(self, request, id) 24 | self.num_requests = 0 25 | 26 | def start_request (self, request): 27 | 28 | # This is called from the main object publishing loop whenever 29 | # we start processing a new request. Obviously, this is a good 30 | # place to track the number of requests made. (If we were 31 | # interested in the number of *successful* requests made, then 32 | # we could override finish_request(), which is called by 33 | # the publisher at the end of each successful request.) 34 | 35 | Session.start_request(self, request) 36 | self.num_requests += 1 37 | 38 | def has_info (self): 39 | 40 | # Overriding has_info() is essential but non-obvious. The 41 | # session manager uses has_info() to know if it should hang on 42 | # to a session object or not: if a session is "dirty", then it 43 | # must be saved. This prevents saving sessions that don't need 44 | # to be saved, which is especially important as a defensive 45 | # measure against clients that don't handle cookies: without it, 46 | # we might create and store a new session object for every 47 | # request made by such clients. With has_info(), we create the 48 | # new session object every time, but throw it away unsaved as 49 | # soon as the request is complete. 50 | # 51 | # (Of course, if you write your session class such that 52 | # has_info() always returns true after a request has been 53 | # processed, you're back to the original problem -- and in fact, 54 | # this class *has* been written that way, because num_requests 55 | # is incremented on every request, which makes has_info() return 56 | # true, which makes SessionManager always store the session 57 | # object. In a real application, think carefully before putting 58 | # data in a session object that causes has_info() to return 59 | # true.) 60 | 61 | return (self.num_requests > 0) or Session.has_info(self) 62 | 63 | is_dirty = has_info 64 | 65 | 66 | class DirMapping: 67 | """A mapping object that stores values as individual pickle 68 | files all in one directory. You wouldn't want to use this in 69 | production unless you're using a filesystem optimized for 70 | handling large numbers of small files, like ReiserFS. However, 71 | it's pretty easy to implement and understand, it doesn't require 72 | any external libraries, and it's really easy to browse the 73 | "database". 74 | """ 75 | 76 | def __init__ (self, save_dir=None): 77 | self.set_save_dir(save_dir) 78 | self.cache = {} 79 | self.cache_time = {} 80 | 81 | def set_save_dir (self, save_dir): 82 | self.save_dir = save_dir 83 | if save_dir and not os.path.isdir(save_dir): 84 | os.mkdir(save_dir, 0700) 85 | 86 | def keys (self): 87 | return os.listdir(self.save_dir) 88 | 89 | def values (self): 90 | # This is pretty expensive! 91 | return [self[id] for id in self.keys()] 92 | 93 | def items (self): 94 | return [(id, self[id]) for id in self.keys()] 95 | 96 | def _gen_filename (self, session_id): 97 | return os.path.join(self.save_dir, session_id) 98 | 99 | def __getitem__ (self, session_id): 100 | 101 | filename = self._gen_filename(session_id) 102 | if (self.cache.has_key(session_id) and 103 | os.stat(filename)[ST_MTIME] <= self.cache_time[session_id]): 104 | return self.cache[session_id] 105 | 106 | if os.path.exists(filename): 107 | try: 108 | file = open(filename, "rb") 109 | try: 110 | print "loading session from %r" % file 111 | session = load(file) 112 | self.cache[session_id] = session 113 | self.cache_time[session_id] = time() 114 | return session 115 | finally: 116 | file.close() 117 | except IOError, err: 118 | raise KeyError(session_id, 119 | "error reading session from %s: %s" 120 | % (filename, err)) 121 | else: 122 | raise KeyError(session_id, 123 | "no such file %s" % filename) 124 | 125 | def get (self, session_id, default=None): 126 | try: 127 | return self[session_id] 128 | except KeyError: 129 | return default 130 | 131 | def has_key (self, session_id): 132 | return os.path.exists(self._gen_filename(session_id)) 133 | 134 | def __setitem__ (self, session_id, session): 135 | filename = self._gen_filename(session.id) 136 | file = open(filename, "wb") 137 | print "saving session to %s" % file 138 | dump(session, file, 1) 139 | file.close() 140 | 141 | self.cache[session_id] = session 142 | self.cache_time[session_id] = time() 143 | 144 | def __delitem__ (self, session_id): 145 | filename = self._gen_filename(session_id) 146 | if os.path.exists(filename): 147 | os.remove(filename) 148 | if self.cache.has_key(session_id): 149 | del self.cache[session_id] 150 | del self.cache_time[session_id] 151 | else: 152 | raise KeyError(session_id, "no such file: %s" % filename) 153 | 154 | 155 | # This is mostly the same as the standard boilerplate for any Quixote 156 | # driver script. The main difference is that we have to instantiate a 157 | # session manager, and use SessionPublisher instead of the normal 158 | # Publisher class. Just like demo.cgi, we use demo.conf to setup log 159 | # files and ensure that error messages are more informative than secure. 160 | 161 | # You can use the 'shelve' module to create an alternative persistent 162 | # mapping to the DirMapping class above. 163 | #import shelve 164 | #sessions = shelve.open("/tmp/quixote-sessions") 165 | 166 | enable_ptl() 167 | sessions = DirMapping(save_dir="/tmp/quixote-session-demo") 168 | session_mgr = SessionManager(session_class=DemoSession, 169 | session_mapping=sessions) 170 | app = SessionPublisher('quixote.demo.session', session_mgr=session_mgr) 171 | app.read_config("demo.conf") 172 | app.setup_logs() 173 | app.publish_cgi() 174 | -------------------------------------------------------------------------------- /quixote/_py_htmltext.py: -------------------------------------------------------------------------------- 1 | """Python implementation of the htmltext type, the htmlescape function and 2 | TemplateIO. 3 | """ 4 | 5 | #$HeadURL: svn+ssh://svn/repos/trunk/quixote/_py_htmltext.py $ 6 | #$Id$ 7 | 8 | import sys 9 | from types import UnicodeType, TupleType, StringType, IntType, FloatType, \ 10 | LongType 11 | import re 12 | 13 | if sys.hexversion < 0x20200b1: 14 | # 2.2 compatibility hacks 15 | class object: 16 | pass 17 | 18 | def classof(o): 19 | if hasattr(o, "__class__"): 20 | return o.__class__ 21 | else: 22 | return type(o) 23 | 24 | else: 25 | classof = type 26 | 27 | _format_codes = 'diouxXeEfFgGcrs%' 28 | _format_re = re.compile(r'%%[^%s]*[%s]' % (_format_codes, _format_codes)) 29 | 30 | def _escape_string(s): 31 | if not isinstance(s, StringType): 32 | raise TypeError, 'string required' 33 | s = s.replace("&", "&") 34 | s = s.replace("<", "<") 35 | s = s.replace(">", ">") 36 | s = s.replace('"', """) 37 | return s 38 | 39 | class htmltext(object): 40 | """The htmltext string-like type. This type serves as a tag 41 | signifying that HTML special characters do not need to be escaped 42 | using entities. 43 | """ 44 | 45 | __slots__ = ['s'] 46 | 47 | def __init__(self, s): 48 | self.s = str(s) 49 | 50 | # XXX make read-only 51 | #def __setattr__(self, name, value): 52 | # raise AttributeError, 'immutable object' 53 | 54 | def __getstate__(self): 55 | raise ValueError, 'htmltext objects should not be pickled' 56 | 57 | def __repr__(self): 58 | return '' % self.s 59 | 60 | def __str__(self): 61 | return self.s 62 | 63 | def __len__(self): 64 | return len(self.s) 65 | 66 | def __cmp__(self, other): 67 | return cmp(self.s, other) 68 | 69 | def __hash__(self): 70 | return hash(self.s) 71 | 72 | def __mod__(self, args): 73 | codes = [] 74 | usedict = 0 75 | for format in _format_re.findall(self.s): 76 | if format[-1] != '%': 77 | if format[1] == '(': 78 | usedict = 1 79 | codes.append(format[-1]) 80 | if usedict: 81 | args = _DictWrapper(args) 82 | else: 83 | if len(codes) == 1 and not isinstance(args, TupleType): 84 | args = (args,) 85 | args = tuple([_wraparg(arg) for arg in args]) 86 | return self.__class__(self.s % args) 87 | 88 | def __add__(self, other): 89 | if isinstance(other, StringType): 90 | return self.__class__(self.s + _escape_string(other)) 91 | elif classof(other) is self.__class__: 92 | return self.__class__(self.s + other.s) 93 | else: 94 | return NotImplemented 95 | 96 | def __radd__(self, other): 97 | if isinstance(other, StringType): 98 | return self.__class__(_escape_string(other) + self.s) 99 | else: 100 | return NotImplemented 101 | 102 | def __mul__(self, n): 103 | return self.__class__(self.s * n) 104 | 105 | def join(self, items): 106 | quoted_items = [] 107 | for item in items: 108 | if classof(item) is self.__class__: 109 | quoted_items.append(str(item)) 110 | elif isinstance(item, StringType): 111 | quoted_items.append(_escape_string(item)) 112 | else: 113 | raise TypeError( 114 | 'join() requires string arguments (got %r)' % item) 115 | return self.__class__(self.s.join(quoted_items)) 116 | 117 | def startswith(self, s): 118 | if isinstance(s, htmltext): 119 | s = s.s 120 | else: 121 | s = _escape_string(s) 122 | return self.s.startswith(s) 123 | 124 | def endswith(self, s): 125 | if isinstance(s, htmltext): 126 | s = s.s 127 | else: 128 | s = _escape_string(s) 129 | return self.s.endswith(s) 130 | 131 | def replace(self, old, new, maxsplit=-1): 132 | if isinstance(old, htmltext): 133 | old = old.s 134 | else: 135 | old = _escape_string(old) 136 | if isinstance(new, htmltext): 137 | new = new.s 138 | else: 139 | new = _escape_string(new) 140 | return self.__class__(self.s.replace(old, new)) 141 | 142 | def lower(self): 143 | return self.__class__(self.s.lower()) 144 | 145 | def upper(self): 146 | return self.__class__(self.s.upper()) 147 | 148 | def capitalize(self): 149 | return self.__class__(self.s.capitalize()) 150 | 151 | class _QuoteWrapper(object): 152 | # helper for htmltext class __mod__ 153 | 154 | __slots__ = ['value', 'escape'] 155 | 156 | def __init__(self, value, escape): 157 | self.value = value 158 | self.escape = escape 159 | 160 | def __str__(self): 161 | return self.escape(str(self.value)) 162 | 163 | def __repr__(self): 164 | return self.escape(`self.value`) 165 | 166 | class _DictWrapper(object): 167 | def __init__(self, value): 168 | self.value = value 169 | 170 | def __getitem__(self, key): 171 | return _wraparg(self.value[key]) 172 | 173 | def _wraparg(arg): 174 | if (classof(arg) is htmltext or 175 | isinstance(arg, IntType) or 176 | isinstance(arg, LongType) or 177 | isinstance(arg, FloatType)): 178 | # ints, longs, floats, and htmltext are okay 179 | return arg 180 | else: 181 | # everything is gets wrapped 182 | return _QuoteWrapper(arg, _escape_string) 183 | 184 | def htmlescape(s): 185 | """htmlescape(s) -> htmltext 186 | 187 | Return an 'htmltext' object using the argument. If the argument is not 188 | already a 'htmltext' object then the HTML markup characters \", <, >, 189 | and & are first escaped. 190 | """ 191 | if classof(s) is htmltext: 192 | return s 193 | elif isinstance(s, UnicodeType): 194 | s = s.encode('iso-8859-1') 195 | else: 196 | s = str(s) 197 | # inline _escape_string for speed 198 | s = s.replace("&", "&") # must be done first 199 | s = s.replace("<", "<") 200 | s = s.replace(">", ">") 201 | s = s.replace('"', """) 202 | return htmltext(s) 203 | 204 | 205 | class TemplateIO(object): 206 | """Collect output for PTL scripts. 207 | """ 208 | 209 | __slots__ = ['html', 'data'] 210 | 211 | def __init__(self, html=0): 212 | self.html = html 213 | self.data = [] 214 | 215 | def __iadd__(self, other): 216 | if other is not None: 217 | self.data.append(other) 218 | return self 219 | 220 | def __repr__(self): 221 | return ("<%s at %x: %d chunks>" % 222 | (self.__class__.__name__, id(self), len(self.data))) 223 | 224 | def __str__(self): 225 | return str(self.getvalue()) 226 | 227 | def getvalue(self): 228 | if self.html: 229 | return htmltext('').join(map(htmlescape, self.data)) 230 | else: 231 | return ''.join(map(str, self.data)) 232 | -------------------------------------------------------------------------------- /doc/upload.txt: -------------------------------------------------------------------------------- 1 | HTTP Upload with Quixote 2 | ======================== 3 | 4 | Starting with Quixote 0.5.1, Quixote has a new mechanism for handling 5 | HTTP upload requests. The bad news is that Quixote applications that 6 | already handle file uploads will have to change; the good news is that 7 | the new way is much simpler, saner, and more efficient. 8 | 9 | As (vaguely) specified by RFC 1867, HTTP upload requests are implemented 10 | by transmitting requests with a Content-Type header of 11 | ``multipart/form-data``. (Normal HTTP form-processing requests have a 12 | Content-Type of ``application/x-www-form-urlencoded``.) Since this type 13 | of request is generally only used for file uploads, Quixote 0.5.1 14 | introduced a new class for dealing with it: HTTPUploadRequest, a 15 | subclass of HTTPRequest. 16 | 17 | 18 | Upload Form 19 | ----------- 20 | 21 | Here's how it works: first, you create a form that will be encoded 22 | according to RFC 1867, ie. with ``multipart/form-data``. You can put 23 | any ordinary form elements there, but for a file upload to take place, 24 | you need to supply at least one ``file`` form element. Here's an 25 | example:: 26 | 27 | def upload_form [html] (request): 28 | ''' 29 |
32 | Your name:
33 |
34 | File to upload:
35 |
36 | 37 | 38 | ''' 39 | 40 | (You can use Quixote's widget classes to construct the non-``file`` form 41 | elements, but the Form class currently doesn't know about the 42 | ``enctype`` attribute, so it's not much use here. Also, you can supply 43 | multiple ``file`` widgets to upload multiple files simultaneously.) 44 | 45 | The user fills out this form as usual; most browsers let the user either 46 | enter a filename or select a file from a dialog box. But when the form 47 | is submitted, the browser creates an HTTP request that is different from 48 | other HTTP requests in two ways: 49 | 50 | * it's encoded according to RFC 1867, i.e. as a MIME message where each 51 | sub-part is one form variable (this is irrelevant to you -- Quixote's 52 | HTTPUploadRequest takes care of the details) 53 | 54 | * it's arbitrarily large -- even for very large and complicated HTML 55 | forms, the HTTP request is usually no more than a few hundred bytes. 56 | With file upload, the uploaded file is included right in the request, 57 | so the HTTP request is as large as the upload, plus a bit of overhead. 58 | 59 | 60 | How Quixote Handles the Upload Request 61 | -------------------------------------- 62 | 63 | When Quixote sees an HTTP request with a Content-Type of 64 | ``multipart/form-data``, it creates an HTTPUploadRequest object instead 65 | of the usual HTTPRequest. (This happens even if there's not an uploaded 66 | file in the request -- Quixote doesn't know this when the request object 67 | is created, and ``multipart/form-data`` requests are oddballs that are 68 | better handled by a completely separate class, whether they actually 69 | include an upload or not.) This is the ``request`` object that will be 70 | passed to your form-handling function or template, eg. :: 71 | 72 | def receive [html] (request): 73 | print request 74 | 75 | should print an HTTPUploadRequest object to the debug log, assuming that 76 | ``receive()`` is being invoked as a result of the above form. 77 | 78 | However, since upload requests can be arbitrarily large, it might be 79 | some time before Quixote actually calls ``receive()``. And Quixote has 80 | to interact with the real world in a number of ways in order to parse 81 | the request, so there are a number of opportunities for things to go 82 | wrong. In particular, whenever Quixote sees a file upload variable in 83 | the request, it: 84 | 85 | * checks that the ``UPLOAD_DIR`` configuration variable was defined. 86 | If not, it raises ConfigError. 87 | 88 | * ensures that ``UPLOAD_DIR`` exists, and creates it if not. (It's 89 | created with the mode specified by ``UPLOAD_DIR_MODE``, which defaults 90 | to ``0755``. I have no idea what this should be on Windows.) If this 91 | fails, your application will presumably crash with an OSError. 92 | 93 | * opens a temporary file in ``UPLOAD_DIR`` and write the contents 94 | of the uploaded file to it. Either opening or writing could fail 95 | with IOError. 96 | 97 | Furthermore, if there are any problems parsing the request body -- which 98 | could be the result of either a broken/malicious client or of a bug in 99 | HTTPUploadRequest -- then Quixote raises RequestError. 100 | 101 | These errors are treated the same as any other exception Quixote 102 | encounters: RequestError (which is a subclass of PublishError) is 103 | transformed into a "400 Invalid request" HTTP response, and the others 104 | become some form of "internal server error" response, with traceback 105 | optionally shown to the user, emailed to you, etc. 106 | 107 | 108 | Processing the Upload Request 109 | ----------------------------- 110 | 111 | If Quixote successfully parses the upload request, then it passes a 112 | ``request`` object to some function or PTL template that you supply, as 113 | usual. Of course, that ``request`` object will be an instance of 114 | HTTPUploadRequest rather than HTTPRequest, but that doesn't make much 115 | difference to you. You can access form variables, cookies, etc. just as 116 | you usually do. The only difference is that form variables associated 117 | with uploaded files are represented as Upload objects. Here's an 118 | example that goes with the above upload form:: 119 | 120 | def receive [html] (request): 121 | name = request.form.get("name") 122 | if name: 123 | "

Thanks, %s!

\n" % name 124 | 125 | upload = request.form.get("upload") 126 | size = os.stat(upload.tmp_filename)[stat.ST_SIZE] 127 | if not upload.base_filename or size == 0: 128 | "

You appear not to have uploaded anything.

\n" 129 | else: 130 | '''\ 131 |

You just uploaded %s (%d bytes)
132 | which is temporarily stored in %s.

133 | ''' % (upload.base_filename, size, upload.tmp_filename) 134 | 135 | Upload objects provide three attributes of interest: 136 | 137 | ``orig_filename`` 138 | the complete filename supplied by the user-agent in the request that 139 | uploaded this file. Depending on the browser, this might have the 140 | complete path of the original file on the client system, in the client 141 | system's syntax -- eg. ``C:\foo\bar\upload_this`` or 142 | ``/foo/bar/upload_this`` or ``foo:bar:upload_this``. 143 | 144 | ``base_filename`` 145 | the base component of orig_filename, shorn of MS-DOS, Mac OS, and Unix 146 | path components and with "unsafe" characters replaced with 147 | underscores. (The "safe" characters are ``A-Z``, ``a-z``, ``0-9``, 148 | ``- @ & + = _ .``, and space. Thus, this is "safe" in the sense that 149 | it's OK to create a filename with any of those characters on Unix, Mac 150 | OS, and Windows, *not* in the sense that you can use the filename in 151 | an HTML document without quoting it!) 152 | 153 | ``tmp_filename`` 154 | where you'll actually find the file on the current system 155 | 156 | Thus, you could open the file directly using ``tmp_filename``, or move 157 | it to a permanent location using ``tmp_filename`` and ``base_filename`` 158 | -- whatever. 159 | 160 | 161 | Upload Demo 162 | ----------- 163 | 164 | The above upload form and form-processor are available, in a slightly 165 | different form, in ``demo/upload.cgi``. Install that file to your usual 166 | ``cgi-bin`` directory and play around. 167 | 168 | $Id: upload.txt 20217 2003-01-16 20:51:53Z akuchlin $ 169 | -------------------------------------------------------------------------------- /doc/utest_html.py: -------------------------------------------------------------------------------- 1 | #!/www/python/bin/python 2 | """ 3 | $URL: svn+ssh://svn/repos/trunk/quixote/test/utest_html.py $ 4 | $Id$ 5 | """ 6 | from sancho.utest import UTest 7 | from quixote import _py_htmltext 8 | 9 | escape = htmlescape = None # so that checker does not complain 10 | 11 | class Wrapper: 12 | def __init__(self, s): 13 | self.s = s 14 | 15 | def __repr__(self): 16 | return self.s 17 | 18 | def __str__(self): 19 | return self.s 20 | 21 | class Broken: 22 | def __str__(self): 23 | raise RuntimeError, 'eieee' 24 | 25 | def __repr__(self): 26 | raise RuntimeError, 'eieee' 27 | 28 | markupchars = '<>&"' 29 | quotedchars = '<>&"' 30 | 31 | class HTMLTest (UTest): 32 | 33 | def _pre(self): 34 | global htmltext, escape, htmlescape 35 | htmltext = _py_htmltext.htmltext 36 | escape = _py_htmltext._escape_string 37 | htmlescape = _py_htmltext.htmlescape 38 | 39 | def _post(self): 40 | pass 41 | 42 | 43 | def check_init(self): 44 | assert str(htmltext('foo')) == 'foo' 45 | assert str(htmltext(markupchars)) == markupchars 46 | assert str(htmltext(None)) == 'None' 47 | assert str(htmltext(1)) == '1' 48 | try: 49 | htmltext(Broken()) 50 | assert 0 51 | except RuntimeError: pass 52 | 53 | def check_escape(self): 54 | assert htmlescape(markupchars) == quotedchars 55 | assert isinstance(htmlescape(markupchars), htmltext) 56 | assert escape(markupchars) == quotedchars 57 | assert isinstance(escape(markupchars), str) 58 | assert htmlescape(htmlescape(markupchars)) == quotedchars 59 | try: 60 | escape(1) 61 | assert 0 62 | except TypeError: pass 63 | 64 | def check_cmp(self): 65 | s = htmltext("foo") 66 | assert s == 'foo' 67 | assert s != 'bar' 68 | assert s == htmltext('foo') 69 | assert s != htmltext('bar') 70 | assert htmltext('1') != 1 71 | assert 1 != s 72 | 73 | def check_len(self): 74 | assert len(htmltext('foo')) == 3 75 | assert len(htmltext(markupchars)) == len(markupchars) 76 | assert len(htmlescape(markupchars)) == len(quotedchars) 77 | 78 | def check_hash(self): 79 | assert hash(htmltext('foo')) == hash('foo') 80 | assert hash(htmltext(markupchars)) == hash(markupchars) 81 | assert hash(htmlescape(markupchars)) == hash(quotedchars) 82 | 83 | def check_concat(self): 84 | s = htmltext("foo") 85 | assert s + 'bar' == "foobar" 86 | assert 'bar' + s == "barfoo" 87 | assert s + htmltext('bar') == "foobar" 88 | assert s + markupchars == "foo" + quotedchars 89 | assert isinstance(s + markupchars, htmltext) 90 | assert markupchars + s == quotedchars + "foo" 91 | assert isinstance(markupchars + s, htmltext) 92 | try: 93 | s + 1 94 | assert 0 95 | except TypeError: pass 96 | try: 97 | 1 + s 98 | assert 0 99 | except TypeError: pass 100 | 101 | def check_repeat(self): 102 | s = htmltext('a') 103 | assert s * 3 == "aaa" 104 | assert isinstance(s * 3, htmltext) 105 | assert htmlescape(markupchars) * 3 == quotedchars * 3 106 | try: 107 | s * 'a' 108 | assert 0 109 | except TypeError: pass 110 | try: 111 | 'a' * s 112 | assert 0 113 | except TypeError: pass 114 | try: 115 | s * s 116 | assert 0 117 | except TypeError: pass 118 | 119 | def check_format(self): 120 | s_fmt = htmltext('%s') 121 | assert s_fmt % 'foo' == "foo" 122 | assert isinstance(s_fmt % 'foo', htmltext) 123 | assert s_fmt % markupchars == quotedchars 124 | assert s_fmt % None == "None" 125 | assert htmltext('%r') % Wrapper(markupchars) == quotedchars 126 | assert htmltext('%s%s') % ('foo', htmltext(markupchars)) == ( 127 | "foo" + markupchars) 128 | assert htmltext('%d') % 10 == "10" 129 | assert htmltext('%.1f') % 10 == "10.0" 130 | try: 131 | s_fmt % Broken() 132 | assert 0 133 | except RuntimeError: pass 134 | try: 135 | htmltext('%r') % Broken() 136 | assert 0 137 | except RuntimeError: pass 138 | try: 139 | s_fmt % (1, 2) 140 | assert 0 141 | except TypeError: pass 142 | assert htmltext('%d') % 12300000000000000000L == "12300000000000000000" 143 | 144 | def check_dict_format(self): 145 | assert htmltext('%(a)s %(a)r %(b)s') % ( 146 | {'a': 'foo&', 'b': htmltext('bar&')}) == "foo& 'foo&' bar&" 147 | assert htmltext('%(a)s') % {'a': 'foo&'} == "foo&" 148 | assert isinstance(htmltext('%(a)s') % {'a': 'a'}, htmltext) 149 | assert htmltext('%s') % {'a': 'foo&'} == "{'a': 'foo&'}" 150 | try: 151 | htmltext('%(a)s') % 1 152 | assert 0 153 | except TypeError: pass 154 | try: 155 | htmltext('%(a)s') % {} 156 | assert 0 157 | except KeyError: pass 158 | 159 | def check_join(self): 160 | assert htmltext(' ').join(['foo', 'bar']) == "foo bar" 161 | assert htmltext(' ').join(['foo', markupchars]) == ( 162 | "foo " + quotedchars) 163 | assert htmlescape(markupchars).join(['foo', 'bar']) == ( 164 | "foo" + quotedchars + "bar") 165 | assert htmltext(' ').join([htmltext(markupchars), 'bar']) == ( 166 | markupchars + " bar") 167 | assert isinstance(htmltext('').join([]), htmltext) 168 | try: 169 | htmltext('').join(1) 170 | assert 0 171 | except TypeError: pass 172 | try: 173 | htmltext('').join([1]) 174 | assert 0 175 | except TypeError: pass 176 | 177 | def check_startswith(self): 178 | assert htmltext('foo').startswith('fo') 179 | assert htmlescape(markupchars).startswith(markupchars[:3]) 180 | assert htmltext(markupchars).startswith(htmltext(markupchars[:3])) 181 | try: 182 | htmltext('').startswith(1) 183 | assert 0 184 | except TypeError: pass 185 | 186 | def check_endswith(self): 187 | assert htmltext('foo').endswith('oo') 188 | assert htmlescape(markupchars).endswith(markupchars[-3:]) 189 | assert htmltext(markupchars).endswith(htmltext(markupchars[-3:])) 190 | try: 191 | htmltext('').endswith(1) 192 | assert 0 193 | except TypeError: pass 194 | 195 | def check_replace(self): 196 | assert htmlescape('&').replace('&', 'foo') == "foo" 197 | assert htmltext('&').replace(htmltext('&'), 'foo') == "foo" 198 | assert htmltext('foo').replace('foo', htmltext('&')) == "&" 199 | assert isinstance(htmltext('a').replace('a', 'b'), htmltext) 200 | try: 201 | htmltext('').replace(1, 'a') 202 | assert 0 203 | except TypeError: pass 204 | 205 | def check_lower(self): 206 | assert htmltext('aB').lower() == "ab" 207 | assert isinstance(htmltext('a').lower(), htmltext) 208 | 209 | def check_upper(self): 210 | assert htmltext('aB').upper() == "AB" 211 | assert isinstance(htmltext('a').upper(), htmltext) 212 | 213 | def check_capitalize(self): 214 | assert htmltext('aB').capitalize() == "Ab" 215 | assert isinstance(htmltext('a').capitalize(), htmltext) 216 | 217 | 218 | try: 219 | from quixote import _c_htmltext 220 | except ImportError: 221 | _c_htmltext = None 222 | 223 | if _c_htmltext: 224 | class CHTMLTest(HTMLTest): 225 | def _pre(self): 226 | # using globals like this is a bit of a hack since it assumes 227 | # Sancho tests each class individually, oh well 228 | global htmltext, escape, htmlescape 229 | htmltext = _c_htmltext.htmltext 230 | escape = _c_htmltext._escape_string 231 | htmlescape = _c_htmltext.htmlescape 232 | 233 | if __name__ == "__main__": 234 | HTMLTest() 235 | -------------------------------------------------------------------------------- /doc/web-services.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Implementing Web Services with Quixote 8 | 9 | 10 | 11 |
12 |

Implementing Web Services with Quixote

13 |

This document will show you how to implement Web services using 14 | Quixote.

15 |
16 |

An XML-RPC Service

17 |

XML-RPC is the simplest protocol commonly used to expose a Web 18 | service. In XML-RPC, there are a few basic data types such as 19 | integers, floats, strings, and dates, and a few aggregate types such 20 | as arrays and structs. The xmlrpclib module, part of the Python 2.2 21 | standard library and available separately from 22 | http://www.pythonware.com/products/xmlrpc/, converts between Python's 23 | standard data types and the XML-RPC data types.

24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
XML-RPC TypePython Type or Class
<int>int
<double>float
<string>string
<array>list
<struct>dict
<boolean>xmlrpclib.Boolean
<base64>xmlrpclib.Binary
<dateTime>xmlrpclib.DateTime
59 | 60 |
61 |

Making XML-RPC Calls

62 |

Making an XML-RPC call using xmlrpclib is easy. An XML-RPC server 63 | lives at a particular URL, so the first step is to create an 64 | xmlrpclib.ServerProxy object pointing at that URL.

65 |
 66 | >>> import xmlrpclib
 67 | >>> s = xmlrpclib.ServerProxy(
 68 |              'http://www.stuffeddog.com/speller/speller-rpc.cgi')
 69 | 
70 |

Now you can simply make a call to the spell-checking service offered 71 | by this server:

72 |
 73 | >>> s.speller.spellCheck('my speling isnt gud', {})
 74 | [{'word': 'speling', 'suggestions': ['apeling', 'spelding',
 75 |   'spelling', 'sperling', 'spewing', 'spiling'], 'location': 4},
 76 | {'word': 'isnt', 'suggestions': [``isn't'', 'ist'], 'location': 12}]
 77 | >>> 
 78 | 
79 |

This call results in the following XML being sent:

80 |
 81 | <?xml version='1.0'?>
 82 | <methodCall>
 83 |      <methodName>speller.spellCheck</methodName>
 84 |      <params>
 85 |          <param>
 86 |                 <value><string>my speling isnt gud</string></value>
 87 |          </param>
 88 |          <param>
 89 |                  <value><struct></struct></value>
 90 |          </param>
 91 |      </params>
 92 | </methodCall>
 93 | 
94 |
95 |
96 |

Writing a Quixote Service

97 |

In the quixote.util module, Quixote provides a function, 98 | xmlrpc(request, func), that processes the body of an XML-RPC 99 | request. request is the HTTPRequest object that Quixote passes to 100 | every function it invokes. func is a user-supplied function that 101 | receives the name of the XML-RPC method being called and a tuple 102 | containing the method's parameters. If there's a bug in the function 103 | you supply and it raises an exception, the xmlrpc() function will 104 | catch the exception and return a Fault to the remote caller.

105 |

Here's an example of implementing a simple XML-RPC handler with a 106 | single method, get_time(), that simply returns the current 107 | time. The first task is to expose a URL for accessing the service.

108 |
109 | from quixote.util import xmlrpc
110 | 
111 | _q_exports = ['rpc']
112 | 
113 | def rpc (request):
114 |     return xmlrpc(request, rpc_process)
115 | 
116 | def rpc_process (meth, params):
117 |     ...
118 | 
119 |

When the above code is placed in the __init__.py file for the Python 120 | package corresponding to your Quixote application, it exposes the URL 121 | http://<hostname>/rpc as the access point for the XML-RPC service.

122 |

Next, we need to fill in the contents of the rpc_process() 123 | function:

124 |
125 | import time
126 | 
127 | def rpc_process (meth, params):
128 |     if meth == 'get_time':
129 |         # params is ignored
130 |         now = time.gmtime(time.time())
131 |         return xmlrpclib.DateTime(now)
132 |     else:
133 |         raise RuntimeError, "Unknown XML-RPC method: %r" % meth
134 | 
135 |

rpc_process() receives the method name and the parameters, and its 136 | job is to run the right code for the method, returning a result that 137 | will be marshalled into XML-RPC. The body of rpc_process() will 138 | therefore usually be an if statement that checks the name of the 139 | method, and calls another function to do the actual work. In this case, 140 | get_time() is very simple so the two lines of code it requires are 141 | simply included in the body of rpc_process().

142 |

If the method name doesn't belong to a supported method, execution 143 | will fall through to the else clause, which will raise a 144 | RuntimeError exception. Quixote's xmlrpc() will catch this 145 | exception and report it to the caller as an XML-RPC fault, with the 146 | error code set to 1.

147 |

As you add additional XML-RPC services, the if statement in 148 | rpc_process() will grow more branches. You might be tempted to pass 149 | the method name to getattr() to select a method from a module or 150 | class. That would work, too, and avoids having a continually growing 151 | set of branches, but you should be careful with this and be sure that 152 | there are no private methods that a remote caller could access. I 153 | generally prefer to have the if... elif... elif... else blocks, for 154 | three reasons: 1) adding another branch isn't much work, 2) it's 155 | explicit about the supported method names, and 3) there won't be any 156 | security holes in doing so.

157 |

An alternative approach is to have a dictionary mapping method names 158 | to the corresponding functions and restrict the legal method names 159 | to the keys of this dictionary:

160 |
161 | def echo (*params):
162 |     # Just returns the parameters it's passed
163 |     return params
164 | 
165 | def get_time ():
166 |     now = time.gmtime(time.time())
167 |     return xmlrpclib.DateTime(now)
168 | 
169 | methods = {'echo' : echo, 
170 |            'get_time' : get_time}
171 | 
172 | def rpc_process (meth, params):
173 |     func = methods.get[meth]
174 |     if methods.has_key(meth):
175 |         # params is ignored
176 |         now = time.gmtime(time.time())
177 |         return xmlrpclib.DateTime(now)
178 |     else:
179 |         raise RuntimeError, "Unknown XML-RPC method: %r" % meth
180 | 
181 |

This approach works nicely when there are many methods and the 182 | if...elif...else statement would be unworkably long.

183 |

$Id: web-services.txt 21603 2003-05-09 19:17:04Z akuchlin $

184 |
185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /quixote/server/twisted_http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | twist -- Demo of an HTTP server built on top of Twisted Python. 5 | """ 6 | 7 | __revision__ = "$Id$" 8 | 9 | # based on qserv, created 2002/03/19, AMK 10 | # last mod 2003.03.24, Graham Fawcett 11 | # tested on Win32 / Twisted 0.18.0 / Quixote 0.6b5 12 | # 13 | # version 0.2 -- 2003.03.24 11:07 PM 14 | # adds missing support for session management, and for 15 | # standard Quixote response headers (expires, date) 16 | # 17 | # modified 2004/04/10 jsibre 18 | # better support for Streams 19 | # wraps output (whether Stream or not) into twisted type producer. 20 | # modified to use reactor instead of Application (Appication 21 | # has been deprecated) 22 | 23 | import urllib 24 | from twisted.protocols import http 25 | from twisted.web import server 26 | 27 | # imports for the TWProducer object 28 | from twisted.spread import pb 29 | from twisted.python import threadable 30 | from twisted.internet import abstract 31 | 32 | from quixote.http_response import Stream 33 | 34 | class QuixoteTWRequest(server.Request): 35 | 36 | def process(self): 37 | self.publisher = self.channel.factory.publisher 38 | environ = self.create_environment() 39 | # this seek is important, it doesn't work without it (it doesn't 40 | # matter for GETs, but POSTs will not work properly without it.) 41 | self.content.seek(0, 0) 42 | qxrequest = self.publisher.create_request(self.content, environ) 43 | self.quixote_publish(qxrequest, environ) 44 | resp = qxrequest.response 45 | self.setResponseCode(resp.status_code) 46 | for hdr, value in resp.generate_headers(): 47 | self.setHeader(hdr, value) 48 | if resp.body is not None: 49 | TWProducer(resp.body, self) 50 | else: 51 | self.finish() 52 | 53 | 54 | def quixote_publish(self, qxrequest, env): 55 | """ 56 | Warning, this sidesteps the Publisher.publish method, 57 | Hope you didn't override it... 58 | """ 59 | pub = self.publisher 60 | output = pub.process_request(qxrequest, env) 61 | 62 | # don't write out the output, just set the response body 63 | # the calling method will do the rest. 64 | if output: 65 | qxrequest.response.set_body(output) 66 | 67 | pub._clear_request() 68 | 69 | 70 | def create_environment(self): 71 | """ 72 | Borrowed heavily from twisted.web.twcgi 73 | """ 74 | # Twisted doesn't decode the path for us, 75 | # so let's do it here. This is also 76 | # what medusa_http.py does, right or wrong. 77 | if '%' in self.path: 78 | self.path = urllib.unquote(self.path) 79 | 80 | serverName = self.getRequestHostname().split(':')[0] 81 | env = {"SERVER_SOFTWARE": server.version, 82 | "SERVER_NAME": serverName, 83 | "GATEWAY_INTERFACE": "CGI/1.1", 84 | "SERVER_PROTOCOL": self.clientproto, 85 | "SERVER_PORT": str(self.getHost()[2]), 86 | "REQUEST_METHOD": self.method, 87 | "SCRIPT_NAME": '', 88 | "SCRIPT_FILENAME": '', 89 | "REQUEST_URI": self.uri, 90 | "HTTPS": (self.isSecure() and 'on') or 'off', 91 | "ACCEPT_ENCODING": self.getHeader('Accept-encoding'), 92 | 'CONTENT_TYPE': self.getHeader('Content-type'), 93 | 'HTTP_COOKIE': self.getHeader('Cookie'), 94 | 'HTTP_REFERER': self.getHeader('Referer'), 95 | 'HTTP_USER_AGENT': self.getHeader('User-agent'), 96 | 'SERVER_PROTOCOL': 'HTTP/1.1', 97 | } 98 | 99 | client = self.getClient() 100 | if client is not None: 101 | env['REMOTE_HOST'] = client 102 | ip = self.getClientIP() 103 | if ip is not None: 104 | env['REMOTE_ADDR'] = ip 105 | xx, xx, remote_port = self.transport.getPeer() 106 | env['REMOTE_PORT'] = remote_port 107 | env["PATH_INFO"] = self.path 108 | 109 | qindex = self.uri.find('?') 110 | if qindex != -1: 111 | env['QUERY_STRING'] = self.uri[qindex+1:] 112 | else: 113 | env['QUERY_STRING'] = '' 114 | 115 | # Propogate HTTP headers 116 | for title, header in self.getAllHeaders().items(): 117 | envname = title.replace('-', '_').upper() 118 | if title not in ('content-type', 'content-length'): 119 | envname = "HTTP_" + envname 120 | env[envname] = header 121 | 122 | return env 123 | 124 | 125 | class TWProducer(pb.Viewable): 126 | """ 127 | A class to represent the transfer of data over the network. 128 | 129 | JES Note: This has more stuff in it than is minimally neccesary. 130 | However, since I'm no twisted guru, I built this by modifing 131 | twisted.web.static.FileTransfer. FileTransfer has stuff in it 132 | that I don't really understand, but know that I probably don't 133 | need. I'm leaving it in under the theory that if anyone ever 134 | needs that stuff (e.g. because they're running with multiple 135 | threads) it'll be MUCH easier for them if I had just left it in 136 | than if they have to figure out what needs to be in there. 137 | Furthermore, I notice no performance penalty for leaving it in. 138 | """ 139 | request = None 140 | def __init__(self, data, request): 141 | self.request = request 142 | self.data = "" 143 | self.size = 0 144 | self.stream = None 145 | self.streamIter = None 146 | 147 | self.outputBufferSize = abstract.FileDescriptor.bufferSize 148 | 149 | if isinstance(data, Stream): # data could be a Stream 150 | self.stream = data 151 | self.streamIter = iter(data) 152 | self.size = data.length 153 | elif data: # data could be a string 154 | self.data = data 155 | self.size = len(data) 156 | else: # data could be None 157 | # We'll just leave self.data as "" 158 | pass 159 | 160 | request.registerProducer(self, 0) 161 | 162 | 163 | def resumeProducing(self): 164 | """ 165 | This is twisted's version of a producer's '.more()', or 166 | an iterator's '.next()'. That is, this function is 167 | responsible for returning some content. 168 | """ 169 | if not self.request: 170 | return 171 | 172 | if self.stream: 173 | # If we were provided a Stream, let's grab some data 174 | # and push it into our data buffer 175 | 176 | buffer = [self.data] 177 | bytesInBuffer = len(buffer[-1]) 178 | while bytesInBuffer < self.outputBufferSize: 179 | try: 180 | buffer.append(self.streamIter.next()) 181 | bytesInBuffer += len(buffer[-1]) 182 | except StopIteration: 183 | # We've exhausted the Stream, time to clean up. 184 | self.stream = None 185 | self.streamIter = None 186 | break 187 | self.data = "".join(buffer) 188 | 189 | if self.data: 190 | chunkSize = min(self.outputBufferSize, len(self.data)) 191 | data, self.data = self.data[:chunkSize], self.data[chunkSize:] 192 | else: 193 | data = "" 194 | 195 | if data: 196 | self.request.write(data) 197 | 198 | if not self.data: 199 | self.request.unregisterProducer() 200 | self.request.finish() 201 | self.request = None 202 | 203 | def pauseProducing(self): 204 | pass 205 | 206 | def stopProducing(self): 207 | self.data = "" 208 | self.request = None 209 | self.stream = None 210 | self.streamIter = None 211 | 212 | # Remotely relay producer interface. 213 | 214 | def view_resumeProducing(self, issuer): 215 | self.resumeProducing() 216 | 217 | def view_pauseProducing(self, issuer): 218 | self.pauseProducing() 219 | 220 | def view_stopProducing(self, issuer): 221 | self.stopProducing() 222 | 223 | synchronized = ['resumeProducing', 'stopProducing'] 224 | 225 | threadable.synchronize(TWProducer) 226 | 227 | 228 | 229 | class QuixoteFactory(http.HTTPFactory): 230 | 231 | def __init__(self, publisher): 232 | self.publisher = publisher 233 | http.HTTPFactory.__init__(self, None) 234 | 235 | def buildProtocol(self, addr): 236 | p = http.HTTPFactory.buildProtocol(self, addr) 237 | p.requestFactory = QuixoteTWRequest 238 | return p 239 | 240 | 241 | def Server(namespace, http_port): 242 | from twisted.internet import reactor 243 | from quixote.publish import Publisher 244 | 245 | # If you want SSL, make sure you have OpenSSL, 246 | # uncomment the follownig, and uncomment the 247 | # listenSSL() call below. 248 | 249 | ##from OpenSSL import SSL 250 | ##class ServerContextFactory: 251 | ## def getContext(self): 252 | ## ctx = SSL.Context(SSL.SSLv23_METHOD) 253 | ## ctx.use_certificate_file('/path/to/pem/encoded/ssl_cert_file') 254 | ## ctx.use_privatekey_file('/path/to/pem/encoded/ssl_key_file') 255 | ## return ctx 256 | 257 | publisher = Publisher(namespace) 258 | ##publisher.setup_logs() 259 | qf = QuixoteFactory(publisher) 260 | 261 | reactor.listenTCP(http_port, qf) 262 | ##reactor.listenSSL(http_port, qf, ServerContextFactory()) 263 | 264 | return reactor 265 | 266 | 267 | def run(namespace, port): 268 | app = Server(namespace, port) 269 | app.run() 270 | 271 | 272 | if __name__ == '__main__': 273 | from quixote import enable_ptl 274 | enable_ptl() 275 | run('quixote.demo', 8080) 276 | -------------------------------------------------------------------------------- /doc/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HTTP Upload with Quixote 8 | 9 | 10 | 11 |
12 |

HTTP Upload with Quixote

13 |

Starting with Quixote 0.5.1, Quixote has a new mechanism for handling 14 | HTTP upload requests. The bad news is that Quixote applications that 15 | already handle file uploads will have to change; the good news is that 16 | the new way is much simpler, saner, and more efficient.

17 |

As (vaguely) specified by RFC 1867, HTTP upload requests are implemented 18 | by transmitting requests with a Content-Type header of 19 | multipart/form-data. (Normal HTTP form-processing requests have a 20 | Content-Type of application/x-www-form-urlencoded.) Since this type 21 | of request is generally only used for file uploads, Quixote 0.5.1 22 | introduced a new class for dealing with it: HTTPUploadRequest, a 23 | subclass of HTTPRequest.

24 |
25 |

Upload Form

26 |

Here's how it works: first, you create a form that will be encoded 27 | according to RFC 1867, ie. with multipart/form-data. You can put 28 | any ordinary form elements there, but for a file upload to take place, 29 | you need to supply at least one file form element. Here's an 30 | example:

31 |
 32 | def upload_form [html] (request):
 33 |     '''
 34 |     <form enctype="multipart/form-data"
 35 |           method="POST" 
 36 |           action="receive">
 37 |       Your name:<br>
 38 |       <input type="text" name="name"><br>
 39 |       File to upload:<br>
 40 |       <input type="file" name="upload"><br>
 41 |       <input type="submit" value="Upload">
 42 |     </form>
 43 |     '''
 44 | 
45 |

(You can use Quixote's widget classes to construct the non-file form 46 | elements, but the Form class currently doesn't know about the 47 | enctype attribute, so it's not much use here. Also, you can supply 48 | multiple file widgets to upload multiple files simultaneously.)

49 |

The user fills out this form as usual; most browsers let the user either 50 | enter a filename or select a file from a dialog box. But when the form 51 | is submitted, the browser creates an HTTP request that is different from 52 | other HTTP requests in two ways:

53 |
    54 |
  • it's encoded according to RFC 1867, i.e. as a MIME message where each 55 | sub-part is one form variable (this is irrelevant to you -- Quixote's 56 | HTTPUploadRequest takes care of the details)
  • 57 |
  • it's arbitrarily large -- even for very large and complicated HTML 58 | forms, the HTTP request is usually no more than a few hundred bytes. 59 | With file upload, the uploaded file is included right in the request, 60 | so the HTTP request is as large as the upload, plus a bit of overhead.
  • 61 |
62 |
63 |
64 |

How Quixote Handles the Upload Request

65 |

When Quixote sees an HTTP request with a Content-Type of 66 | multipart/form-data, it creates an HTTPUploadRequest object instead 67 | of the usual HTTPRequest. (This happens even if there's not an uploaded 68 | file in the request -- Quixote doesn't know this when the request object 69 | is created, and multipart/form-data requests are oddballs that are 70 | better handled by a completely separate class, whether they actually 71 | include an upload or not.) This is the request object that will be 72 | passed to your form-handling function or template, eg.

73 |
 74 | def receive [html] (request):
 75 |     print request
 76 | 
77 |

should print an HTTPUploadRequest object to the debug log, assuming that 78 | receive() is being invoked as a result of the above form.

79 |

However, since upload requests can be arbitrarily large, it might be 80 | some time before Quixote actually calls receive(). And Quixote has 81 | to interact with the real world in a number of ways in order to parse 82 | the request, so there are a number of opportunities for things to go 83 | wrong. In particular, whenever Quixote sees a file upload variable in 84 | the request, it:

85 |
    86 |
  • checks that the UPLOAD_DIR configuration variable was defined. 87 | If not, it raises ConfigError.
  • 88 |
  • ensures that UPLOAD_DIR exists, and creates it if not. (It's 89 | created with the mode specified by UPLOAD_DIR_MODE, which defaults 90 | to 0755. I have no idea what this should be on Windows.) If this 91 | fails, your application will presumably crash with an OSError.
  • 92 |
  • opens a temporary file in UPLOAD_DIR and write the contents 93 | of the uploaded file to it. Either opening or writing could fail 94 | with IOError.
  • 95 |
96 |

Furthermore, if there are any problems parsing the request body -- which 97 | could be the result of either a broken/malicious client or of a bug in 98 | HTTPUploadRequest -- then Quixote raises RequestError.

99 |

These errors are treated the same as any other exception Quixote 100 | encounters: RequestError (which is a subclass of PublishError) is 101 | transformed into a "400 Invalid request" HTTP response, and the others 102 | become some form of "internal server error" response, with traceback 103 | optionally shown to the user, emailed to you, etc.

104 |
105 |
106 |

Processing the Upload Request

107 |

If Quixote successfully parses the upload request, then it passes a 108 | request object to some function or PTL template that you supply, as 109 | usual. Of course, that request object will be an instance of 110 | HTTPUploadRequest rather than HTTPRequest, but that doesn't make much 111 | difference to you. You can access form variables, cookies, etc. just as 112 | you usually do. The only difference is that form variables associated 113 | with uploaded files are represented as Upload objects. Here's an 114 | example that goes with the above upload form:

115 |
116 | def receive [html] (request):
117 |     name = request.form.get("name")
118 |     if name:
119 |         "<p>Thanks, %s!</p>\n" % name
120 | 
121 |     upload = request.form.get("upload")
122 |     size = os.stat(upload.tmp_filename)[stat.ST_SIZE]
123 |     if not upload.base_filename or size == 0:
124 |         "<p>You appear not to have uploaded anything.</p>\n"
125 |     else:
126 |         '''\
127 |         <p>You just uploaded <code>%s</code> (%d bytes)<br>
128 |         which is temporarily stored in <code>%s</code>.</p>
129 |         ''' % (upload.base_filename, size, upload.tmp_filename)
130 | 
131 |

Upload objects provide three attributes of interest:

132 |
133 |
orig_filename
134 |
the complete filename supplied by the user-agent in the request that 135 | uploaded this file. Depending on the browser, this might have the 136 | complete path of the original file on the client system, in the client 137 | system's syntax -- eg. C:\foo\bar\upload_this or 138 | /foo/bar/upload_this or foo:bar:upload_this.
139 |
base_filename
140 |
the base component of orig_filename, shorn of MS-DOS, Mac OS, and Unix 141 | path components and with "unsafe" characters replaced with 142 | underscores. (The "safe" characters are A-Z, a-z, 0-9, 143 | - @ & + = _ ., and space. Thus, this is "safe" in the sense that 144 | it's OK to create a filename with any of those characters on Unix, Mac 145 | OS, and Windows, not in the sense that you can use the filename in 146 | an HTML document without quoting it!)
147 |
tmp_filename
148 |
where you'll actually find the file on the current system
149 |
150 |

Thus, you could open the file directly using tmp_filename, or move 151 | it to a permanent location using tmp_filename and base_filename 152 | -- whatever.

153 |
154 |
155 |

Upload Demo

156 |

The above upload form and form-processor are available, in a slightly 157 | different form, in demo/upload.cgi. Install that file to your usual 158 | cgi-bin directory and play around.

159 |

$Id: upload.txt 20217 2003-01-16 20:51:53Z akuchlin $

160 |
161 |
162 | 163 | 164 | -------------------------------------------------------------------------------- /doc/upgrading.txt: -------------------------------------------------------------------------------- 1 | Upgrading code from older versions of Quixote 2 | ============================================= 3 | 4 | This document lists backward-incompatible changes in Quixote, and 5 | explains how to update application code to work with the newer 6 | version. 7 | 8 | Changes from 0.6.1 to 1.0 9 | ------------------------- 10 | 11 | Sessions 12 | ******** 13 | 14 | A leading underscore was removed from the ``Session`` attributes 15 | ``__remote_address``, ``__creation_time``, and ``__access_time``. If 16 | you have pickled ``Session`` objects you will need to upgrade them 17 | somehow. Our preferred method is to write a script that unpickles each 18 | object, renames the attributes and then re-pickles it. 19 | 20 | 21 | 22 | Changes from 0.6 to 0.6.1 23 | ------------------------- 24 | 25 | ``_q_exception_handler`` now called if exception while traversing 26 | ***************************************************************** 27 | 28 | ``_q_exception_handler`` hooks will now be called if an exception is 29 | raised during the traversal process. Quixote 0.6 had a bug that caused 30 | ``_q_exception_handler`` hooks to only be called if an exception was 31 | raised after the traversal completed. 32 | 33 | 34 | 35 | Changes from 0.5 to 0.6 36 | ----------------------- 37 | 38 | ``_q_getname`` renamed to ``_q_lookup`` 39 | *************************************** 40 | 41 | The ``_q_getname`` special function was renamed to ``_q_lookup``, 42 | because that name gives a clearer impression of the function's 43 | purpose. In 0.6, ``_q_getname`` still works but will trigger a 44 | warning. 45 | 46 | 47 | Form Framework Changes 48 | ********************** 49 | 50 | The ``quixote.form.form`` module was changed from a .ptl file to a .py 51 | file. You should delete or move the existing ``quixote/`` directory 52 | in ``site-packages`` before running ``setup.py``, or at least delete 53 | the old ``form.ptl`` and ``form.ptlc`` files. 54 | 55 | The widget and form classes in the ``quixote.form`` package now return 56 | ``htmltext`` instances. Applications that use forms and widgets will 57 | likely have to be changed to use the ``[html]`` template type to avoid 58 | over-escaping of HTML special characters. 59 | 60 | Also, the constructor arguments to ``SelectWidget`` and its subclasses have 61 | changed. This only affects applications that use the form framework 62 | located in the ``quixote.form`` package. 63 | 64 | In Quixote 0.5, the ``SelectWidget`` constructor had this signature:: 65 | 66 | def __init__ (self, name, value=None, 67 | allowed_values=None, 68 | descriptions=None, 69 | size=None, 70 | sort=0): 71 | 72 | ``allowed_values`` was the list of objects that the user could choose, 73 | and ``descriptions`` was a list of strings that would actually be 74 | shown to the user in the generated HTML. 75 | 76 | In Quixote 0.6, the signature has changed slightly:: 77 | 78 | def __init__ (self, name, value=None, 79 | allowed_values=None, 80 | descriptions=None, 81 | options=None, 82 | size=None, 83 | sort=0): 84 | 85 | The ``quote`` argument is gone, and the ``options`` argument has been 86 | added. If an ``options`` argument is provided, ``allowed_values`` 87 | and ``descriptions`` must not be supplied. 88 | 89 | The ``options`` argument, if present, must be a list of tuples with 90 | 1,2, or 3 elements, of the form ``(value:any, description:any, 91 | key:string)``. 92 | 93 | * ``value`` is the object that will be returned if the user chooses 94 | this item, and must always be supplied. 95 | 96 | * ``description`` is a string or htmltext instance which will be 97 | shown to the user in the generated HTML. It will be passed 98 | through the htmlescape() functions, so for an ordinary string 99 | special characters such as '&' will be converted to '&'. 100 | htmltext instances will be left as they are. 101 | 102 | * If supplied, ``key`` will be used in the value attribute 103 | of the option element (``