1 | """Super-light WSGI framework to allow returning a string or Response object.
|
---|
2 |
|
---|
3 | The request side of WSGI--the "commons" of the environ mapping--is quite
|
---|
4 | nice. It honors the tradition of CGI, and it's just a mapping. Simple.
|
---|
5 |
|
---|
6 | The response-side API is a little stiffer, because WSGI has to support edge
|
---|
7 | cases like serving large files, complex exception handling, and HTTP/1.1
|
---|
8 | features. This results in warts like start_response, and the requirement
|
---|
9 | that apps return an iterable. The intention is that these warts be smoothed
|
---|
10 | over at other layers, and that's what we're doing here.
|
---|
11 |
|
---|
12 | Apps below this shim may speak plain WSGI, but they may also return a
|
---|
13 | string, which will be sent back as text/html. They may also return or raise
|
---|
14 | a Response object.
|
---|
15 |
|
---|
16 | """
|
---|
17 | __author__ = "Chad Whitacre <chad@zetaweb.com>"
|
---|
18 | __version__ = "~~VERSION~~"
|
---|
19 | __all__ = ('Responder', 'Response')
|
---|
20 |
|
---|
21 |
|
---|
22 | import BaseHTTPServer
|
---|
23 | from email.Message import Message
|
---|
24 |
|
---|
25 |
|
---|
26 | _responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
|
---|
27 |
|
---|
28 |
|
---|
29 | class Response(StandardError):
|
---|
30 | """Represent an HTTP Response message.
|
---|
31 | """
|
---|
32 |
|
---|
33 | def __init__(self, code=200, body='', headers=None):
|
---|
34 | """Takes an int, a string, and a dict.
|
---|
35 |
|
---|
36 | - code an HTTP response code, e.g., 404
|
---|
37 | - body the message body as a string
|
---|
38 | - headers a dictionary of HTTP headers (or list of tuples)
|
---|
39 |
|
---|
40 | Body is second because one more often wants to specify a body without
|
---|
41 | headers, than a header without a body.
|
---|
42 |
|
---|
43 | """
|
---|
44 | if not isinstance(code, int):
|
---|
45 | raise TypeError("'code' must be an integer")
|
---|
46 | elif not isinstance(body, basestring):
|
---|
47 | raise TypeError("'body' must be a string")
|
---|
48 | elif headers is not None and not isinstance(headers, (dict, list)):
|
---|
49 | raise TypeError("'headers' must be a dictionary or a list of " +
|
---|
50 | "2-tuples")
|
---|
51 |
|
---|
52 | StandardError.__init__(self)
|
---|
53 | self.code = code
|
---|
54 | self.body = body
|
---|
55 | self.headers = Message()
|
---|
56 | if headers:
|
---|
57 | if isinstance(headers, dict):
|
---|
58 | headers = headers.items()
|
---|
59 | for k, v in headers:
|
---|
60 | self.headers[k] = v
|
---|
61 |
|
---|
62 |
|
---|
63 | def __repr__(self):
|
---|
64 | return "<Response: %s>" % str(self)
|
---|
65 |
|
---|
66 | def __str__(self):
|
---|
67 | return "%d %s" % (self.code, self._status()[0])
|
---|
68 |
|
---|
69 | def _status(self):
|
---|
70 | return _responses.get(self.code, ('???','Unknown HTTP status'))
|
---|
71 |
|
---|
72 |
|
---|
73 | def __call__(self, environ, start_response):
|
---|
74 | """We ourselves are a WSGI app.
|
---|
75 |
|
---|
76 | XXX: WSGI exception handling?
|
---|
77 |
|
---|
78 | """
|
---|
79 | _status = self._status()
|
---|
80 |
|
---|
81 | status = "%d %s" % (self.code, _status[0])
|
---|
82 | headers = [(str(k), str(v)) for k,v in self.headers.items()]
|
---|
83 | body = [self.body and self.body or _status[1]]
|
---|
84 |
|
---|
85 | start_response(status, headers)
|
---|
86 | return body
|
---|
87 |
|
---|
88 |
|
---|
89 | class Responder(object):
|
---|
90 | """WSGI middleware to also allow returning a string or Response object.
|
---|
91 | """
|
---|
92 |
|
---|
93 | def __init__(self, app):
|
---|
94 | self.wrapped_app = app
|
---|
95 |
|
---|
96 | def __call__(self, environ, start_response):
|
---|
97 | try:
|
---|
98 | response = self.wrapped_app(environ, start_response)
|
---|
99 | except Response, response:
|
---|
100 | pass
|
---|
101 | except:
|
---|
102 | raise
|
---|
103 |
|
---|
104 | if isinstance(response, Response):
|
---|
105 | response = response(environ, start_response)
|
---|
106 | elif isinstance(response, basestring):
|
---|
107 | response = Response(200, response)
|
---|
108 | response.headers['Content-Type'] = 'text/html'
|
---|
109 | response = response(environ, start_response)
|
---|
110 |
|
---|
111 | return response
|
---|
112 |
|
---|
113 |
|
---|
114 | if __name__ == '__main__':
|
---|
115 | """Simple smoke test.
|
---|
116 |
|
---|
117 | Hit http://localhost:8080/ in a web browser after running this script. Only
|
---|
118 | one of the calls to Server can be uncommented or you'll get:
|
---|
119 |
|
---|
120 | socket.error: (48, 'Address already in use')
|
---|
121 |
|
---|
122 | """
|
---|
123 | from wsgiref.simple_server import make_server # included w/ Python 2.5
|
---|
124 |
|
---|
125 | Server = lambda app: make_server('', 8080, app)
|
---|
126 | app = lambda e, s: "Greetings, program!"
|
---|
127 |
|
---|
128 | def app2(environ, start_response):
|
---|
129 | return Response( 200, "Greetings, program!"
|
---|
130 | , {'Content-Type':'text/plain'}
|
---|
131 | )
|
---|
132 |
|
---|
133 | server = Server(Responder(app)) # tests returning a string
|
---|
134 | #server = Server(Responder(app2)) # tests returning a Response
|
---|
135 | #server = Server(app) # unwrapped; raises AssertionError when hit
|
---|
136 |
|
---|
137 | server.serve_forever()
|
---|
138 |
|
---|
139 |
|
---|
140 | """ <http://opensource.org/licenses/mit-license.php>
|
---|
141 |
|
---|
142 | Copyright (c) 2006 Chad Whitacre <chad@zetaweb.com>
|
---|
143 |
|
---|
144 | Permission is hereby granted, free of charge, to any person obtaining a copy of
|
---|
145 | this software and associated documentation files (the "Software"), to deal in
|
---|
146 | the Software without restriction, including without limitation the rights to
|
---|
147 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
---|
148 | the Software, and to permit persons to whom the Software is furnished to do so,
|
---|
149 | subject to the following conditions:
|
---|
150 |
|
---|
151 | The above copyright notice and this permission notice shall be included in all
|
---|
152 | copies or substantial portions of the Software.
|
---|
153 |
|
---|
154 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
---|
155 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
---|
156 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
---|
157 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
---|
158 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
---|
159 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
---|
160 |
|
---|
161 | """
|
---|