1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/WebStack/Adapters/WSGI.py Wed Dec 01 00:56:03 2004 +0000
1.3 @@ -0,0 +1,54 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +WSGI adapter.
1.8 +"""
1.9 +
1.10 +import WebStack.WSGI
1.11 +from WebStack.Generic import EndOfResponse
1.12 +
1.13 +class WSGIAdapter:
1.14 +
1.15 + "A WSGI adapter class."
1.16 +
1.17 + def __init__(self, resource, authenticator=None):
1.18 +
1.19 + """
1.20 + Initialise the adapter with the given WebStack 'resource' and the
1.21 + optional 'authenticator'.
1.22 + """
1.23 +
1.24 + self.resource = resource
1.25 + self.authenticator = authenticator
1.26 +
1.27 + def __call__(self, environ, start_response):
1.28 +
1.29 + """
1.30 + Dispatch to the root application-specific 'resource'. Return a list of
1.31 + strings comprising the response body text.
1.32 + """
1.33 +
1.34 + trans = WebStack.WSGI.Transaction(environ)
1.35 +
1.36 + try:
1.37 + if self.authenticator is None or self.authenticator.authenticate(trans):
1.38 + try:
1.39 + self.resource.respond(trans)
1.40 + except EndOfResponse:
1.41 + pass
1.42 + else:
1.43 + trans.set_response_code(401) # Unauthorized
1.44 + trans.set_header_value("WWW-Authenticate", '%s realm="%s"' % (
1.45 + self.authenticator.get_auth_type(), self.authenticator.get_realm()))
1.46 + finally:
1.47 + trans.commit()
1.48 +
1.49 + # NOTE: Provide sensible messages.
1.50 +
1.51 + start_response(
1.52 + "%s WebStack status" % trans.get_response_code(),
1.53 + trans.get_wsgi_headers()
1.54 + )
1.55 + return [trans.get_wsgi_content()]
1.56 +
1.57 +# vim: tabstop=4 expandtab shiftwidth=4
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/WebStack/WSGI.py Wed Dec 01 00:56:03 2004 +0000
2.3 @@ -0,0 +1,479 @@
2.4 +#!/usr/bin/env python
2.5 +
2.6 +"""
2.7 +WSGI classes.
2.8 +"""
2.9 +
2.10 +import Generic
2.11 +import os, sys
2.12 +from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items, Cookie
2.13 +from Helpers.Response import ConvertingStream
2.14 +from Helpers.Auth import UserInfo
2.15 +from Helpers.Session import SessionStore
2.16 +from Helpers import Environment
2.17 +from cgi import parse_qs, FieldStorage
2.18 +from Cookie import SimpleCookie
2.19 +from StringIO import StringIO
2.20 +
2.21 +class Transaction(Generic.Transaction):
2.22 +
2.23 + """
2.24 + WSGI transaction interface.
2.25 + """
2.26 +
2.27 + def __init__(self, env):
2.28 +
2.29 + """
2.30 + Initialise the transaction using the given WSGI environment 'env'.
2.31 + """
2.32 +
2.33 + self.env = env
2.34 +
2.35 + # Other attributes of interest in instances of this class.
2.36 +
2.37 + self.content_type = None
2.38 + self.response_code = 200
2.39 + self.content = StringIO()
2.40 + self.headers_out = {}
2.41 + self.cookies_out = SimpleCookie()
2.42 + self.user = None
2.43 +
2.44 + # Define the incoming cookies.
2.45 +
2.46 + self.cookies_in = SimpleCookie(self.env.get("HTTP_COOKIE"))
2.47 +
2.48 + # Cached information.
2.49 +
2.50 + self.storage_body = None
2.51 +
2.52 + # Special objects retained throughout the transaction.
2.53 +
2.54 + self.session_store = None
2.55 +
2.56 + def commit(self):
2.57 +
2.58 + """
2.59 + A special method, synchronising the transaction with framework-specific
2.60 + objects.
2.61 + """
2.62 +
2.63 + # Close the session store.
2.64 +
2.65 + if self.session_store is not None:
2.66 + self.session_store.close()
2.67 +
2.68 + def get_wsgi_headers(self):
2.69 + wsgi_headers = []
2.70 +
2.71 + if self.content_type is not None:
2.72 + wsgi_headers.append(("Content-type", str(self.content_type)))
2.73 +
2.74 + for header, value in self.headers_out.items():
2.75 + wsgi_headers.append(
2.76 + (self.format_header_value(header), self.format_header_value(value))
2.77 + )
2.78 +
2.79 + # NOTE: Nasty deconstruction of Morsel values.
2.80 +
2.81 + for value in self.cookies_out.values():
2.82 + parts = str(value).split(": ")
2.83 + wsgi_headers.append(
2.84 + (parts[0], ": ".join(parts[1:]))
2.85 + )
2.86 +
2.87 + return wsgi_headers
2.88 +
2.89 + def get_wsgi_content(self):
2.90 + self.content.seek(0)
2.91 + return self.content.read()
2.92 +
2.93 + # Request-related methods.
2.94 +
2.95 + def get_request_stream(self):
2.96 +
2.97 + """
2.98 + Returns the request stream for the transaction.
2.99 + """
2.100 +
2.101 + return self.env["wsgi.input"]
2.102 +
2.103 + def get_request_method(self):
2.104 +
2.105 + """
2.106 + Returns the request method.
2.107 + """
2.108 +
2.109 + return self.env.get("REQUEST_METHOD")
2.110 +
2.111 + def get_headers(self):
2.112 +
2.113 + """
2.114 + Returns all request headers as a dictionary-like object mapping header
2.115 + names to values.
2.116 + """
2.117 +
2.118 + return Environment.get_headers(self.env)
2.119 +
2.120 + def get_header_values(self, key):
2.121 +
2.122 + """
2.123 + Returns a list of all request header values associated with the given
2.124 + 'key'. Note that according to RFC 2616, 'key' is treated as a
2.125 + case-insensitive string.
2.126 + """
2.127 +
2.128 + return self.convert_to_list(self.get_headers().get(key))
2.129 +
2.130 + def get_content_type(self):
2.131 +
2.132 + """
2.133 + Returns the content type specified on the request, along with the
2.134 + charset employed.
2.135 + """
2.136 +
2.137 + return self.parse_content_type(self.env.get("CONTENT_TYPE"))
2.138 +
2.139 + def get_content_charsets(self):
2.140 +
2.141 + """
2.142 + Returns the character set preferences.
2.143 + """
2.144 +
2.145 + return self.parse_content_preferences(None)
2.146 +
2.147 + def get_content_languages(self):
2.148 +
2.149 + """
2.150 + Returns extracted language information from the transaction.
2.151 + """
2.152 +
2.153 + return self.parse_content_preferences(None)
2.154 +
2.155 + def get_path(self):
2.156 +
2.157 + """
2.158 + Returns the entire path from the request.
2.159 + """
2.160 +
2.161 + path = self.get_path_without_query()
2.162 + qs = self.get_query_string()
2.163 + if qs:
2.164 + path += "?"
2.165 + path += qs
2.166 + return path
2.167 +
2.168 + def get_path_without_query(self):
2.169 +
2.170 + """
2.171 + Returns the entire path from the request minus the query string.
2.172 + """
2.173 +
2.174 + path = self.env.get("SCRIPT_NAME") or ""
2.175 + if self.env.has_key("PATH_INFO"):
2.176 + path += self.env["PATH_INFO"]
2.177 + return path
2.178 +
2.179 + def get_path_info(self):
2.180 +
2.181 + """
2.182 + Returns the "path info" (the part of the URL after the resource name
2.183 + handling the current request) from the request.
2.184 + """
2.185 +
2.186 + return self.env.get("PATH_INFO") or ""
2.187 +
2.188 + def get_query_string(self):
2.189 +
2.190 + """
2.191 + Returns the query string from the path in the request.
2.192 + """
2.193 +
2.194 + return self.env.get("QUERY_STRING") or ""
2.195 +
2.196 + # Higher level request-related methods.
2.197 +
2.198 + def get_fields_from_path(self):
2.199 +
2.200 + """
2.201 + Extracts fields (or request parameters) from the path specified in the
2.202 + transaction. The underlying framework may refuse to supply fields from
2.203 + the path if handling a POST transaction.
2.204 +
2.205 + Returns a dictionary mapping field names to lists of values (even if a
2.206 + single value is associated with any given field name).
2.207 + """
2.208 +
2.209 + # NOTE: Support at best ISO-8859-1 values.
2.210 +
2.211 + fields = {}
2.212 + for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items():
2.213 + fields[name] = []
2.214 + for value in values:
2.215 + fields[name].append(unicode(value, "iso-8859-1"))
2.216 + return fields
2.217 +
2.218 + def get_fields_from_body(self, encoding=None):
2.219 +
2.220 + """
2.221 + Extracts fields (or request parameters) from the message body in the
2.222 + transaction. The optional 'encoding' parameter specifies the character
2.223 + encoding of the message body for cases where no such information is
2.224 + available, but where the default encoding is to be overridden.
2.225 +
2.226 + Returns a dictionary mapping field names to lists of values (even if a
2.227 + single value is associated with any given field name). Each value is
2.228 + either a Unicode object (representing a simple form field, for example)
2.229 + or a plain string (representing a file upload form field, for example).
2.230 + """
2.231 +
2.232 + encoding = encoding or self.get_content_type().charset or self.default_charset
2.233 +
2.234 + if self.storage_body is None:
2.235 + self.storage_body = FieldStorage(fp=self.get_request_stream(),
2.236 + headers={"content-type" : str(self.get_content_type())},
2.237 + environ={"REQUEST_METHOD" : self.get_request_method()},
2.238 + keep_blank_values=1)
2.239 +
2.240 + # Avoid strange design issues with FieldStorage by checking the internal
2.241 + # field list directly.
2.242 +
2.243 + fields = {}
2.244 + if self.storage_body.list is not None:
2.245 +
2.246 + # Traverse the storage, finding each field value.
2.247 +
2.248 + fields = get_body_fields(get_storage_items(self.storage_body), encoding)
2.249 +
2.250 + return fields
2.251 +
2.252 + def get_fields(self, encoding=None):
2.253 +
2.254 + """
2.255 + Extracts fields (or request parameters) from both the path specified in
2.256 + the transaction as well as the message body. The optional 'encoding'
2.257 + parameter specifies the character encoding of the message body for cases
2.258 + where no such information is available, but where the default encoding
2.259 + is to be overridden.
2.260 +
2.261 + Returns a dictionary mapping field names to lists of values (even if a
2.262 + single value is associated with any given field name). Each value is
2.263 + either a Unicode object (representing a simple form field, for example)
2.264 + or a plain string (representing a file upload form field, for example).
2.265 +
2.266 + Where a given field name is used in both the path and message body to
2.267 + specify values, the values from both sources will be combined into a
2.268 + single list associated with that field name.
2.269 + """
2.270 +
2.271 + # Combine the two sources.
2.272 +
2.273 + fields = {}
2.274 + fields.update(self.get_fields_from_path())
2.275 + for name, values in self.get_fields_from_body(encoding).items():
2.276 + if not fields.has_key(name):
2.277 + fields[name] = values
2.278 + else:
2.279 + fields[name] += values
2.280 + return fields
2.281 +
2.282 + def get_user(self):
2.283 +
2.284 + """
2.285 + Extracts user information from the transaction.
2.286 +
2.287 + Returns a username as a string or None if no user is defined.
2.288 + """
2.289 +
2.290 + if self.user is not None:
2.291 + return self.user
2.292 + else:
2.293 + return self.env.get("REMOTE_USER")
2.294 +
2.295 + def get_cookies(self):
2.296 +
2.297 + """
2.298 + Obtains cookie information from the request.
2.299 +
2.300 + Returns a dictionary mapping cookie names to cookie objects.
2.301 + """
2.302 +
2.303 + return self.process_cookies(self.cookies_in)
2.304 +
2.305 + def get_cookie(self, cookie_name):
2.306 +
2.307 + """
2.308 + Obtains cookie information from the request.
2.309 +
2.310 + Returns a cookie object for the given 'cookie_name' or None if no such
2.311 + cookie exists.
2.312 + """
2.313 +
2.314 + cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name))
2.315 + if cookie is not None:
2.316 + return Cookie(cookie_name, self.decode_cookie_value(cookie.value))
2.317 + else:
2.318 + return None
2.319 +
2.320 + # Response-related methods.
2.321 +
2.322 + def get_response_stream(self):
2.323 +
2.324 + """
2.325 + Returns the response stream for the transaction.
2.326 + """
2.327 +
2.328 + # Return a stream which is later emptied into the real stream.
2.329 + # Unicode can upset this operation. Using either the specified charset
2.330 + # or a default encoding.
2.331 +
2.332 + encoding = self.get_response_stream_encoding()
2.333 + return ConvertingStream(self.content, encoding)
2.334 +
2.335 + def get_response_stream_encoding(self):
2.336 +
2.337 + """
2.338 + Returns the response stream encoding.
2.339 + """
2.340 +
2.341 + if self.content_type:
2.342 + encoding = self.content_type.charset
2.343 + else:
2.344 + encoding = None
2.345 + return encoding or self.default_charset
2.346 +
2.347 + def get_response_code(self):
2.348 +
2.349 + """
2.350 + Get the response code associated with the transaction. If no response
2.351 + code is defined, None is returned.
2.352 + """
2.353 +
2.354 + return self.response_code
2.355 +
2.356 + def set_response_code(self, response_code):
2.357 +
2.358 + """
2.359 + Set the 'response_code' using a numeric constant defined in the HTTP
2.360 + specification.
2.361 + """
2.362 +
2.363 + self.response_code = response_code
2.364 +
2.365 + def set_header_value(self, header, value):
2.366 +
2.367 + """
2.368 + Set the HTTP 'header' with the given 'value'.
2.369 + """
2.370 +
2.371 + # The header is not written out immediately due to the buffering in use.
2.372 +
2.373 + self.headers_out[header] = value
2.374 +
2.375 + def set_content_type(self, content_type):
2.376 +
2.377 + """
2.378 + Sets the 'content_type' for the response.
2.379 + """
2.380 +
2.381 + # The content type has to be written as a header, before actual content,
2.382 + # but after the response line. This means that some kind of buffering is
2.383 + # required. Hence, we don't write the header out immediately.
2.384 +
2.385 + self.content_type = content_type
2.386 +
2.387 + # Higher level response-related methods.
2.388 +
2.389 + def set_cookie(self, cookie):
2.390 +
2.391 + """
2.392 + Stores the given 'cookie' object in the response.
2.393 + """
2.394 +
2.395 + # NOTE: If multiple cookies of the same name could be specified, this
2.396 + # NOTE: could need changing.
2.397 +
2.398 + self.set_cookie_value(cookie.name, cookie.value)
2.399 +
2.400 + def set_cookie_value(self, name, value, path=None, expires=None):
2.401 +
2.402 + """
2.403 + Stores a cookie with the given 'name' and 'value' in the response.
2.404 +
2.405 + The optional 'path' is a string which specifies the scope of the cookie,
2.406 + and the optional 'expires' parameter is a value compatible with the
2.407 + time.time function, and indicates the expiry date/time of the cookie.
2.408 + """
2.409 +
2.410 + name = self.encode_cookie_value(name)
2.411 + self.cookies_out[name] = self.encode_cookie_value(value)
2.412 + if path is not None:
2.413 + self.cookies_out[name]["path"] = path
2.414 + if expires is not None:
2.415 + self.cookies_out[name]["expires"] = expires
2.416 +
2.417 + def delete_cookie(self, cookie_name):
2.418 +
2.419 + """
2.420 + Adds to the response a request that the cookie with the given
2.421 + 'cookie_name' be deleted/discarded by the client.
2.422 + """
2.423 +
2.424 + # Create a special cookie, given that we do not know whether the browser
2.425 + # has been sent the cookie or not.
2.426 + # NOTE: Magic discovered in Webware.
2.427 +
2.428 + name = self.encode_cookie_value(cookie_name)
2.429 + self.cookies_out[name] = ""
2.430 + self.cookies_out[name]["path"] = "/"
2.431 + self.cookies_out[name]["expires"] = 0
2.432 + self.cookies_out[name]["max-age"] = 0
2.433 +
2.434 + # Session-related methods.
2.435 +
2.436 + def get_session(self, create=1):
2.437 +
2.438 + """
2.439 + Gets a session corresponding to an identifier supplied in the
2.440 + transaction.
2.441 +
2.442 + If no session has yet been established according to information
2.443 + provided in the transaction then the optional 'create' parameter
2.444 + determines whether a new session will be established.
2.445 +
2.446 + Where no session has been established and where 'create' is set to 0
2.447 + then None is returned. In all other cases, a session object is created
2.448 + (where appropriate) and returned.
2.449 + """
2.450 +
2.451 + # NOTE: Requires configuration.
2.452 +
2.453 + if self.session_store is None:
2.454 + self.session_store = SessionStore(self, "WebStack-sessions")
2.455 + return self.session_store.get_session(create)
2.456 +
2.457 + def expire_session(self):
2.458 +
2.459 + """
2.460 + Expires any session established according to information provided in the
2.461 + transaction.
2.462 + """
2.463 +
2.464 + # NOTE: Requires configuration.
2.465 +
2.466 + if self.session_store is None:
2.467 + self.session_store = SessionStore(self, "WebStack-sessions")
2.468 + self.session_store.expire_session()
2.469 +
2.470 + # Application-specific methods.
2.471 +
2.472 + def set_user(self, username):
2.473 +
2.474 + """
2.475 + An application-specific method which sets the user information with
2.476 + 'username' in the transaction. This affects subsequent calls to
2.477 + 'get_user'.
2.478 + """
2.479 +
2.480 + self.user = username
2.481 +
2.482 +# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/docs/WSGI/NOTES.txt Wed Dec 01 00:56:03 2004 +0000
3.3 @@ -0,0 +1,11 @@
3.4 +Specifying the appropriate PYTHONPATH, invoke the application program. For
3.5 +example, in the WebStack distribution directory:
3.6 +
3.7 +PYTHONPATH=.:examples/Common python examples/WSGI/SimpleApp.py
3.8 +
3.9 +The WebStack package must reside on the PYTHONPATH, along with the package
3.10 +containing the application itself.
3.11 +
3.12 +In addition, WSGI examples currently require wsgiServer from WSGI Utils to
3.13 +either reside on the PYTHONPATH or to have been installed using the WSGI
3.14 +Utils setup script.