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 |
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:
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:
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:
(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 |
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).
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:
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.
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 |
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 |
session id
user
num requests
93 | ''' % (repr(mgr), len(session_ids))
94 | for sess_id in session_ids:
95 | sess = mgr[sess_id]
96 | ('
%s
%s
%d
\n'
97 | % (sess.id,
98 | sess.user and sess.user or "none",
99 | sess.num_requests))
100 | '
\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 | '
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 |
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.
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.
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.
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.
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:
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:
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.
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:
(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.
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.
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.
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.
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.
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 (``