1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
35 """ Misuse of the gateway """
36
38 """ Attempt to deliver data without calling start_response """
39
41 """ The response was already started """
42
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
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
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
86
87
88 self._baseenv = tuple(self._populate_base_env(base_env).iteritems())
89
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
135 if not responder.started:
136 iterator = iter(iterator)
137 try:
138 chunk = iterator.next()
139 except StopIteration:
140
141
142 chunk, ilen = "", -1
143 else:
144 try:
145 ilen = len(iterator)
146 except TypeError:
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
157 for chunk in iterator:
158 if chunk:
159 responder.write(chunk)
160 finally:
161 close()
162
163
164 if not responder.started:
165 responder.write_headers("", True)
166
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
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
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
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
271 def fget(self):
272 if self._response is None:
273 raise ResponseNotStarted()
274 return self._response[1]
275 return locals()
276
277 @Property
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
288 def fget(self):
289 if self._response is None:
290 raise ResponseNotStarted()
291 return self._response[0]
292 return locals()
293
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
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