1b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik# (c) 2005 Clark C. Evans
4b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik# This module is part of the Python Paste Project and is released under
5b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik# the MIT License: http://www.opensource.org/licenses/mit-license.php
6b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik# This code was written with funding by http://prometheusresearch.com
7b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik"""
8b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris CraikUpload Progress Monitor
9b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
10b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris CraikThis is a WSGI middleware component which monitors the status of files
11b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikbeing uploaded.  It includes a small query application which will return
12b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craika list of all files being uploaded by particular session/user.
13b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
14b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> from paste.httpserver import serve
15b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> from paste.urlmap import URLMap
16b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> from paste.auth.basic import AuthBasicHandler
17b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> from paste.debug.debugapp import SlowConsumer, SimpleApplication
18b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> # from paste.progress import *
19b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> realm = 'Test Realm'
20b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> def authfunc(username, password):
21b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik...     return username == password
22b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> map = URLMap({})
23b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> ups = UploadProgressMonitor(map, threshold=1024)
24b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> map['/upload'] = SlowConsumer()
25b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> map['/simple'] = SimpleApplication()
26b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> map['/report'] = UploadProgressReporter(ups)
27b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik>>> serve(AuthBasicHandler(ups, realm, authfunc))
28b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikserving on...
29b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
30b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik.. note::
31b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
32b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik   This is experimental, and will change in the future.
33b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik"""
34b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikimport time
35b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikfrom paste.wsgilib import catch_errors
36b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
37b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris CraikDEFAULT_THRESHOLD = 1024 * 1024  # one megabyte
38b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris CraikDEFAULT_TIMEOUT   = 60*5         # five minutes
39b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris CraikENVIRON_RECEIVED  = 'paste.bytes_received'
40b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris CraikREQUEST_STARTED   = 'paste.request_started'
41b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris CraikREQUEST_FINISHED  = 'paste.request_finished'
42b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
43b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikclass _ProgressFile(object):
44b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    """
45b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    This is the input-file wrapper used to record the number of
46b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    ``paste.bytes_received`` for the given request.
47b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    """
48b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
49b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def __init__(self, environ, rfile):
50b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self._ProgressFile_environ = environ
51b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self._ProgressFile_rfile   = rfile
52b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.flush = rfile.flush
53b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.write = rfile.write
54b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.writelines = rfile.writelines
55b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
56b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def __iter__(self):
57b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        environ = self._ProgressFile_environ
58b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        riter = iter(self._ProgressFile_rfile)
59b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        def iterwrap():
60b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            for chunk in riter:
61b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                environ[ENVIRON_RECEIVED] += len(chunk)
62b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                yield chunk
63b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return iter(iterwrap)
64b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
65b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def read(self, size=-1):
66b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        chunk = self._ProgressFile_rfile.read(size)
67b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk)
68b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return chunk
69b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
70b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def readline(self):
71b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        chunk = self._ProgressFile_rfile.readline()
72b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk)
73b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return chunk
74b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
75b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def readlines(self, hint=None):
76b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        chunk = self._ProgressFile_rfile.readlines(hint)
77b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk)
78b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return chunk
79b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
80b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikclass UploadProgressMonitor(object):
81b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    """
82b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    monitors and reports on the status of uploads in progress
83b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
84b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    Parameters:
85b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
86b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``application``
87b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
88b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This is the next application in the WSGI stack.
89b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
90b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``threshold``
91b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
92b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This is the size in bytes that is needed for the
93b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            upload to be included in the monitor.
94b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
95b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``timeout``
96b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
97b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This is the amount of time (in seconds) that a upload
98b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            remains in the monitor after it has finished.
99b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
100b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    Methods:
101b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
102b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``uploads()``
103b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
104b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This returns a list of ``environ`` dict objects for each
105b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            upload being currently monitored, or finished but whose time
106b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            has not yet expired.
107b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
108b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    For each request ``environ`` that is monitored, there are several
109b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    variables that are stored:
110b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
111b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``paste.bytes_received``
112b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
113b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This is the total number of bytes received for the given
114b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            request; it can be compared with ``CONTENT_LENGTH`` to
115b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            build a percentage complete.  This is an integer value.
116b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
117b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``paste.request_started``
118b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
119b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This is the time (in seconds) when the request was started
120b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            as obtained from ``time.time()``.  One would want to format
121b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            this for presentation to the user, if necessary.
122b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
123b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``paste.request_finished``
124b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
125b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This is the time (in seconds) when the request was finished,
126b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            canceled, or otherwise disconnected.  This is None while
127b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            the given upload is still in-progress.
128b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
129b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    TODO: turn monitor into a queue and purge queue of finished
130b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik          requests that have passed the timeout period.
131b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    """
132b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def __init__(self, application, threshold=None, timeout=None):
133b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.application = application
134b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.threshold = threshold or DEFAULT_THRESHOLD
135b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.timeout   = timeout   or DEFAULT_TIMEOUT
136b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.monitor   = []
137b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
138b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def __call__(self, environ, start_response):
139b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        length = environ.get('CONTENT_LENGTH', 0)
140b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        if length and int(length) > self.threshold:
141b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            # replace input file object
142b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            self.monitor.append(environ)
143b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            environ[ENVIRON_RECEIVED] = 0
144b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            environ[REQUEST_STARTED] = time.time()
145b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            environ[REQUEST_FINISHED] = None
146b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            environ['wsgi.input'] = \
147b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                _ProgressFile(environ, environ['wsgi.input'])
148b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            def finalizer(exc_info=None):
149b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                environ[REQUEST_FINISHED] = time.time()
150b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            return catch_errors(self.application, environ,
151b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                       start_response, finalizer, finalizer)
152b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return self.application(environ, start_response)
153b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
154b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def uploads(self):
155b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return self.monitor
156b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
157b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikclass UploadProgressReporter(object):
158b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    """
159b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    reports on the progress of uploads for a given user
160b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
161b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    This reporter returns a JSON file (for use in AJAX) listing the
162b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    uploads in progress for the given user.  By default, this reporter
163b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    uses the ``REMOTE_USER`` environment to compare between the current
164b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    request and uploads in-progress.  If they match, then a response
165b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    record is formed.
166b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
167b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``match()``
168b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
169b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This member function can be overriden to provide alternative
170b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            matching criteria.  It takes two environments, the first
171b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            is the current request, the second is a current upload.
172b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
173b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        ``report()``
174b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
175b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            This member function takes an environment and builds a
176b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            ``dict`` that will be used to create a JSON mapping for
177b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            the given upload.  By default, this just includes the
178b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            percent complete and the request url.
179b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
180b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    """
181b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def __init__(self, monitor):
182b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        self.monitor   = monitor
183b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
184b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def match(self, search_environ, upload_environ):
185b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        if search_environ.get('REMOTE_USER', None) == \
186b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik           upload_environ.get('REMOTE_USER', 0):
187b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            return True
188b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return False
189b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
190b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def report(self, environ):
191b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        retval = { 'started': time.strftime("%Y-%m-%d %H:%M:%S",
192b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                                time.gmtime(environ[REQUEST_STARTED])),
193b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                   'finished': '',
194b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                   'content_length': environ.get('CONTENT_LENGTH'),
195b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                   'bytes_received': environ[ENVIRON_RECEIVED],
196b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                   'path_info': environ.get('PATH_INFO',''),
197b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                   'query_string': environ.get('QUERY_STRING','')}
198b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        finished = environ[REQUEST_FINISHED]
199b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        if finished:
200b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            retval['finished'] = time.strftime("%Y:%m:%d %H:%M:%S",
201b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                                               time.gmtime(finished))
202b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return retval
203b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
204b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    def __call__(self, environ, start_response):
205b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        body = []
206b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        for map in [self.report(env) for env in self.monitor.uploads()
207b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                                             if self.match(environ, env)]:
208b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            parts = []
209b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            for k, v in map.items():
210b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                v = str(v).replace("\\", "\\\\").replace('"', '\\"')
211b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                parts.append('%s: "%s"' % (k, v))
212b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            body.append("{ %s }" % ", ".join(parts))
213b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        body = "[ %s ]" % ", ".join(body)
214b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        start_response("200 OK", [('Content-Type', 'text/plain'),
215b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                                  ('Content-Length', len(body))])
216b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik        return [body]
217b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
218b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik__all__ = ['UploadProgressMonitor', 'UploadProgressReporter']
219b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
220b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikif "__main__" == __name__:
221b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    import doctest
222b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik    doctest.testmod(optionflags=doctest.ELLIPSIS)
223