Package wtf :: Package impl :: Module _gateway
[hide private]
[frames] | no frames]

Source Code for Module wtf.impl._gateway

  1  # -*- coding: ascii -*- 
  2  # 
  3  # Copyright 2006-2012 
  4  # Andr\xe9 Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  Gateway Implementation 
 19  ====================== 
 20   
 21  Here's the gateway implemented. 
 22  """ 
 23  __author__ = u"Andr\xe9 Malo" 
 24  __docformat__ = "restructuredtext en" 
 25   
 26  import os as _os 
 27  import sys as _sys 
 28  import traceback as _traceback 
 29   
 30  from wtf import Error 
 31  from wtf.util import Property 
32 33 34 -class GatewayError(Error):
35 """ Misuse of the gateway """
36
37 -class ResponseNotStarted(GatewayError):
38 """ Attempt to deliver data without calling start_response """
39
40 -class ResponseAlreadyStarted(GatewayError):
41 """ The response was already started """
42
43 44 -class Gateway(object):
45 """ 46 WS gateway logic 47 48 :IVariables: 49 - `config`: Configuration 50 - `opts`: Command line options 51 - `args`: Positioned command line arguments 52 - `_baseenv`: Base environment (``((key, value), ...)``) 53 54 :Types: 55 - `config`: `wtf.config.Config` 56 - `opts`: ``optparse.OptionContainer`` 57 - `args`: ``list`` 58 - `_baseenv`: ``tuple`` 59 """ 60
61 - def __init__(self, config, opts, args):
62 """ 63 Initialization 64 65 :Parameters: 66 - `config`: Configuration 67 - `opts`: Command line options 68 - `args`: Positioned command line arguments 69 70 :Types: 71 - `config`: `wtf.config.Config` 72 - `opts`: ``optparse.OptionContainer`` 73 - `args`: ``list`` 74 """ 75 self.config, self.opts, self.args = config, opts, args 76 # populate from os environment, but don't pollute with gateway stuff 77 base_env = dict((key, value) for key, value in _os.environ.iteritems() 78 if not key.startswith('HTTP_') 79 and key not in ('CONTENT_LENGTH', 'CONTENT_TYPE') 80 ) 81 base_env.update({ 82 'wsgi.version': (1, 0), 83 'wsgi.errors': _sys.stderr, 84 }) 85 # base_env is used by every request. To make it thread safe, 86 # we need it shallow-copied for every request - making it 87 # readonly is the best way to ensure that. 88 self._baseenv = tuple(self._populate_base_env(base_env).iteritems())
89
90 - def _populate_base_env(self, base_env):
91 """ 92 Modify base_env before it's finalized 93 94 This is for subclasses to allow modifiying the base environment. 95 By default this method is a no-op. 96 97 :Parameters: 98 - `base_env`: The base environment so far 99 100 :Types: 101 - `base_env`: ``dict`` 102 103 :return: The modified base_env (maybe the same dict as passed in) 104 :rtype: ``dict`` 105 """ 106 return base_env
107
108 - def handle(self, connection, request, application):
109 """ 110 Gateway between the request and the application 111 112 :Parameters: 113 - `connection`: Connection instance 114 - `request`: Request instance (only specific subclasses have to 115 understand it) 116 - `application`: WSGI application 117 118 :Types: 119 - `connection`: `Connection` 120 - `request`: any 121 - `application`: ``callable`` 122 """ 123 environ = dict(self._baseenv) 124 renv, start_response = self._init_from_request(connection, request) 125 environ.update(renv) 126 responder = ResponseStarter(start_response) 127 iterator = application(environ, responder) 128 try: 129 close = iterator.close 130 except AttributeError: 131 close = lambda: None 132 133 try: 134 # Try determining content length 135 if not responder.started: 136 iterator = iter(iterator) 137 try: 138 chunk = iterator.next() 139 except StopIteration: 140 # we could say Content-Length: 0, but there might be 141 # a reason not to (like body-less responses) 142 chunk, ilen = "", -1 143 else: 144 try: 145 ilen = len(iterator) 146 except TypeError: # unsized object 147 ilen = -1 148 if ilen == 0 and not responder.started: 149 have_length = 'content-length' in [key.lower() 150 for key, _ in responder.headers] 151 if not have_length: 152 responder.headers.append( 153 ('Content-Length', str(len(chunk))) 154 ) 155 responder.write(chunk) 156 # Pump out 157 for chunk in iterator: 158 if chunk: 159 responder.write(chunk) 160 finally: 161 close() 162 163 # Write at least the headers 164 if not responder.started: 165 responder.write_headers("", True)
166
167 - def _init_from_request(self, connection, request):
168 """ 169 Initialize env and response starter from request implementation 170 171 Subclasses must override the method. 172 173 :Parameters: 174 - `connection`: The connection the request is handled on 175 - `request`: The request instance 176 177 :Types: 178 - `connection`: `Connection` 179 - `request`: any 180 181 :return: A tuple of the request env and a specific response starter 182 (```(dict, callable)``) 183 :rtype: ``dict`` 184 """ 185 raise NotImplementedError()
186
187 188 -class ResponseStarter(object):
189 """ 190 WSGI start_response callable with context 191 192 :IVariables: 193 - `_stream`: Response body stream 194 - `_start_response`: Implementation defined response starter 195 - `_response`: Status and headers supplied by the application 196 - `write`: Current write callable 197 - `started`: Flag indicating whether the response was started or not 198 199 :Types: 200 - `_stream`: ``file`` 201 - `_start_response`: ``callable`` 202 - `_response`: ``tuple`` 203 - `write`: ``callable`` 204 - `started`: ``bool`` 205 """ 206 started, _response, _stream = False, None, None 207
208 - def __init__(self, start_response):
209 """ 210 Initialization 211 212 :Parameters: 213 - `start_response`: Implementation specific response starter; 214 a callable, which takes status and headers and returns a body 215 stream 216 217 :Types: 218 - `start_response`: ``callable`` 219 """ 220 self._start_response = start_response 221 self.write = self.write_initial
222
223 - def __call__(self, status, headers, exc_info=None):
224 """ 225 Actual WSGI start_response function 226 227 :Parameters: 228 - `status`: Status line 229 - `headers`: Header list 230 - `exc_info`: Optional exception (output of ``sys.exc_info()``) 231 232 :Types: 233 - `status`: ``str`` 234 - `headers`: ``list`` 235 - `exc_info`: ``tuple`` 236 237 :return: Write callable (according to PEP 333) 238 :rtype: ``callable`` 239 """ 240 if self._response is not None: 241 if exc_info is None: 242 raise ResponseAlreadyStarted() 243 elif self.started: 244 try: 245 raise exc_info[0], exc_info[1], exc_info[2] 246 finally: 247 exc_info = None 248 else: 249 try: 250 print >> _sys.stderr, \ 251 "start_response() called with exception:\n" + ''.join( 252 _traceback.format_exception(*exc_info) 253 ) 254 finally: 255 exc_info = None 256 self._response = status, headers 257 self.write = self.write_headers 258 return self.write
259 260 @Property
261 - def headers():
262 """ 263 The response headers as supplied by the application 264 265 Exceptions: 266 `ResponseNotStarted` : The response was not started yet 267 268 :Type: ``list`` 269 """ 270 # pylint: disable = E0211, C0111, W0212, W0612 271 def fget(self): 272 if self._response is None: 273 raise ResponseNotStarted() 274 return self._response[1]
275 return locals()
276 277 @Property
278 - def status():
279 """ 280 The response status as supplied by the application 281 282 Exceptions: 283 `ResponseNotStarted` : The response was not started yet 284 285 :Type: ``str`` 286 """ 287 # pylint: disable = E0211, C0111, W0212, W0612 288 def fget(self): 289 if self._response is None: 290 raise ResponseNotStarted() 291 return self._response[0]
292 return locals() 293
294 - def write_initial(self, data):
295 """ 296 Initial write callable - raises an error 297 298 If this method is called as ``.write``, it generates an error, 299 because the gateway was misused (__call__ not executed). 300 301 :Parameters: 302 - `data`: The string to write 303 304 :Types: 305 - `data`: ``str`` 306 307 :Exceptions: 308 - `ResponseNotStarted`: Response was not started properly 309 """ 310 if self.write == self.write_initial: 311 raise ResponseNotStarted() 312 if data: 313 self.write(data)
314
315 - def write_headers(self, data, _do_init=False):
316 """ 317 Secondary write callable - sends headers before real data 318 319 This write callable initializes the response on the first real 320 occurence of data. The ``write`` method will be set directly to 321 the stream's write method after response initialization. 322 323 :Parameters: 324 - `data`: The data to write 325 - `_do_init`: Initialize the headers anyway (regardless of data?) 326 327 :Types: 328 - `data`: ``str`` 329 - `_do_init`: ``bool`` 330 """ 331 if self.write == self.write_headers: 332 if data or _do_init: 333 self.started = True 334 self._stream = self._start_response(*self._response) 335 self.write = self.write_body 336 if data: 337 self.write(data)
338
339 - def write_body(self, data):
340 """ 341 Final write callable 342 343 This adds a flush after every write call to the stream -- as 344 required by the WSGI specification. 345 346 :Parameters: 347 - `data`: The data to write 348 349 :Types: 350 - `data`: ``str`` 351 """ 352 if data: 353 self._stream.write(data) 354 self._stream.flush()
355