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