1 #!/usr/bin/env python 2 3 """ 4 Generic Web framework interfaces. 5 The WebStack architecture consists of the following layers: 6 7 * Framework - The underlying Web framework implementation. 8 * Adapter - Code operating under the particular framework which creates 9 WebStack abstractions and issues them to the application. 10 * Resources - Units of functionality operating within the hosted Web 11 application. 12 13 Resources can act as both content producers within an application and as request 14 dispatchers to other resources; in the latter role, they may be referred to as 15 directors. 16 """ 17 18 class ContentType: 19 20 "A container for content type information." 21 22 def __init__(self, media_type, charset=None, **attributes): 23 24 """ 25 Initialise the container with the given 'media_type', an optional 26 'charset', and optional keyword attributes representing the key=value 27 pairs which qualify content types. 28 """ 29 30 self.media_type = media_type 31 self.charset = charset 32 self.attributes = attributes 33 34 def __str__(self): 35 36 """ 37 Format the content type object, producing a string suitable for the 38 response header field. 39 """ 40 41 l = [] 42 if self.media_type: 43 l.append(self.media_type) 44 if self.charset: 45 l.append("; ") 46 l.append("charset=%s" % self.charset) 47 for name, value in self.attributes.items(): 48 l.append("; ") 49 l.append("%s=%s" % (name, value)) 50 51 # Make sure that only ASCII is used. 52 53 return "".join(l).encode("US-ASCII") 54 55 class Transaction: 56 57 """ 58 A generic transaction interface containing framework-specific methods to be 59 overridden. 60 """ 61 62 # The default charset ties output together with body field interpretation. 63 64 default_charset = "iso-8859-1" 65 66 def commit(self): 67 68 """ 69 A special method, synchronising the transaction with framework-specific 70 objects. 71 """ 72 73 pass 74 75 # Utility methods. 76 77 def parse_content_type(self, content_type_field): 78 79 """ 80 Determine the content type and charset from the supplied 81 'content_type_field' string. 82 """ 83 84 if content_type_field is None: 85 return ContentType(None) 86 87 l = content_type_field.split(";") 88 attributes = {} 89 charset = None 90 91 # Find the charset and remember all other attributes. 92 93 media_type, attributes_str = l[0].strip(), l[1:] 94 95 for attribute_str in attributes_str: 96 t = attribute_str.split("=") 97 if len(t) > 1: 98 name, value = t[0].strip(), t[1].strip() 99 if name == "charset": 100 charset = value 101 else: 102 attributes[name] = value 103 104 return ContentType(media_type, charset, **attributes) 105 106 def format_header_value(self, value): 107 108 """ 109 Format the given header 'value'. Typically, this just ensures the usage 110 of US-ASCII. 111 """ 112 113 return value.encode("US-ASCII") 114 115 def parse_content_preferences(self, accept_preference): 116 117 """ 118 Returns the preferences as requested by the user agent. The preferences are 119 returned as a list of codes in the same order as they appeared in the 120 appropriate environment variable. In other words, the explicit weighting 121 criteria are ignored. 122 123 As the 'accept_preference' parameter, values for language and charset 124 preferences are appropriate. 125 """ 126 127 if accept_preference is None: 128 return [] 129 130 accept_defs = accept_preference.split(",") 131 accept_prefs = [] 132 for accept_def in accept_defs: 133 t = accept_def.split(";") 134 if len(t) >= 1: 135 accept_prefs.append(t[0].strip()) 136 return accept_prefs 137 138 def convert_to_list(self, value): 139 140 """ 141 Returns a single element list containing 'value' if it is not itself a list, a 142 tuple, or None. If 'value' is a list then it is itself returned; if 'value' is a 143 tuple then a new list containing the same elements is returned; if 'value' is None 144 then an empty list is returned. 145 """ 146 147 if type(value) == type([]): 148 return value 149 elif type(value) == type(()): 150 return list(value) 151 elif value is None: 152 return [] 153 else: 154 return [value] 155 156 # Request-related methods. 157 158 def get_request_stream(self): 159 160 """ 161 Returns the request stream for the transaction. 162 """ 163 164 raise NotImplementedError, "get_request_stream" 165 166 def get_request_method(self): 167 168 """ 169 Returns the request method. 170 """ 171 172 raise NotImplementedError, "get_request_method" 173 174 def get_headers(self): 175 176 """ 177 Returns all request headers as a dictionary-like object mapping header 178 names to values. 179 """ 180 181 raise NotImplementedError, "get_headers" 182 183 def get_header_values(self, key): 184 185 """ 186 Returns a list of all request header values associated with the given 187 'key'. Note that according to RFC 2616, 'key' is treated as a 188 case-insensitive string. 189 """ 190 191 raise NotImplementedError, "get_header_values" 192 193 def get_content_type(self): 194 195 """ 196 Returns the content type specified on the request, along with the 197 charset employed. 198 """ 199 200 raise NotImplementedError, "get_content_type" 201 202 def get_content_charsets(self): 203 204 """ 205 Returns the character set preferences. 206 """ 207 208 raise NotImplementedError, "get_content_charsets" 209 210 def get_content_languages(self): 211 212 """ 213 Returns extracted language information from the transaction. 214 """ 215 216 raise NotImplementedError, "get_content_languages" 217 218 def get_path(self): 219 220 """ 221 Returns the entire path from the request. 222 """ 223 224 raise NotImplementedError, "get_path" 225 226 def get_path_without_query(self): 227 228 """ 229 Returns the entire path from the request minus the query string. 230 """ 231 232 raise NotImplementedError, "get_path_without_query" 233 234 def get_path_info(self): 235 236 """ 237 Returns the "path info" (the part of the URL after the resource name 238 handling the current request) from the request. 239 """ 240 241 raise NotImplementedError, "get_path_info" 242 243 def get_query_string(self): 244 245 """ 246 Returns the query string from the path in the request. 247 """ 248 249 raise NotImplementedError, "get_query_string" 250 251 # Higher level request-related methods. 252 253 def get_fields_from_path(self): 254 255 """ 256 Extracts the form fields from the path specified in the transaction. The 257 underlying framework may refuse to supply fields from the path if 258 handling a POST transaction. 259 260 Returns a dictionary mapping field names to lists of values (even if a 261 single value is associated with any given field name). 262 """ 263 264 raise NotImplementedError, "get_fields_from_path" 265 266 def get_fields_from_body(self, encoding=None): 267 268 """ 269 Extracts the form fields from the message body in the transaction. The 270 optional 'encoding' parameter specifies the character encoding of the 271 message body for cases where no such information is available, but where 272 the default encoding is to be overridden. 273 274 Returns a dictionary mapping field names to lists of values (even if a 275 single value is associated with any given field name). Each value is 276 either a Unicode object (representing a simple form field, for example) 277 or a plain string (representing a file upload form field, for example). 278 """ 279 280 raise NotImplementedError, "get_fields_from_body" 281 282 def get_user(self): 283 284 """ 285 Extracts user information from the transaction. 286 287 Returns a username as a string or None if no user is defined. 288 """ 289 290 raise NotImplementedError, "get_user" 291 292 def get_cookies(self): 293 294 """ 295 Obtains cookie information from the request. 296 297 Returns a dictionary mapping cookie names to cookie objects. 298 """ 299 300 raise NotImplementedError, "get_cookies" 301 302 def get_cookie(self, cookie_name): 303 304 """ 305 Obtains cookie information from the request. 306 307 Returns a cookie object for the given 'cookie_name' or None if no such 308 cookie exists. 309 """ 310 311 raise NotImplementedError, "get_cookie" 312 313 # Response-related methods. 314 315 def get_response_stream(self): 316 317 """ 318 Returns the response stream for the transaction. 319 """ 320 321 raise NotImplementedError, "get_response_stream" 322 323 def get_response_code(self): 324 325 """ 326 Get the response code associated with the transaction. If no response 327 code is defined, None is returned. 328 """ 329 330 raise NotImplementedError, "get_response_code" 331 332 def set_response_code(self, response_code): 333 334 """ 335 Set the 'response_code' using a numeric constant defined in the HTTP 336 specification. 337 """ 338 339 raise NotImplementedError, "set_response_code" 340 341 def set_header_value(self, header, value): 342 343 """ 344 Set the HTTP 'header' with the given 'value'. 345 """ 346 347 raise NotImplementedError, "set_header_value" 348 349 def set_content_type(self, content_type): 350 351 """ 352 Sets the 'content_type' for the response. 353 """ 354 355 raise NotImplementedError, "set_content_type" 356 357 # Higher level response-related methods. 358 359 def set_cookie(self, cookie): 360 361 """ 362 Stores the given 'cookie' object in the response. 363 """ 364 365 raise NotImplementedError, "set_cookie" 366 367 def set_cookie_value(self, name, value, path=None, expires=None): 368 369 """ 370 Stores a cookie with the given 'name' and 'value' in the response. 371 372 The optional 'path' is a string which specifies the scope of the cookie, 373 and the optional 'expires' parameter is a value compatible with the 374 time.time function, and indicates the expiry date/time of the cookie. 375 """ 376 377 raise NotImplementedError, "set_cookie_value" 378 379 def delete_cookie(self, cookie_name): 380 381 """ 382 Adds to the response a request that the cookie with the given 383 'cookie_name' be deleted/discarded by the client. 384 """ 385 386 raise NotImplementedError, "delete_cookie" 387 388 # Session-related methods. 389 390 def get_session(self, create=1): 391 392 """ 393 Gets a session corresponding to an identifier supplied in the 394 transaction. 395 396 If no session has yet been established according to information 397 provided in the transaction then the optional 'create' parameter 398 determines whether a new session will be established. 399 400 Where no session has been established and where 'create' is set to 0 401 then None is returned. In all other cases, a session object is created 402 (where appropriate) and returned. 403 """ 404 405 raise NotImplementedError, "get_session" 406 407 def expire_session(self): 408 409 """ 410 Expires any session established according to information provided in the 411 transaction. 412 """ 413 414 raise NotImplementedError, "expire_session" 415 416 # Application-specific methods. 417 418 def set_user(self, username): 419 420 """ 421 An application-specific method which sets the user information with 422 'username' in the transaction. This affects subsequent calls to 423 'get_user'. 424 """ 425 426 raise NotImplementedError, "set_user" 427 428 class Resource: 429 430 "A generic resource interface." 431 432 def respond(self, trans): 433 434 """ 435 An application-specific method which performs activities on the basis of 436 the transaction object 'trans'. 437 """ 438 439 raise NotImplementedError, "respond" 440 441 class Authenticator: 442 443 "A generic authentication component." 444 445 def authenticate(self, trans): 446 447 """ 448 An application-specific method which authenticates the sender of the 449 request described by the transaction object 'trans'. This method should 450 consider 'trans' to be read-only and not attempt to change the state of 451 the transaction. 452 453 If the sender of the request is authenticated successfully, the result 454 of this method evaluates to true; otherwise the result of this method 455 evaluates to false. 456 """ 457 458 raise NotImplementedError, "authenticate" 459 460 def get_auth_type(self): 461 462 """ 463 An application-specific method which returns the authentication type to 464 be used. An example value is 'Basic' which specifies HTTP basic 465 authentication. 466 """ 467 468 raise NotImplementedError, "get_auth_type" 469 470 def get_realm(self): 471 472 """ 473 An application-specific method which returns the name of the realm for 474 which authentication is taking place. 475 """ 476 477 raise NotImplementedError, "get_realm" 478 479 # vim: tabstop=4 expandtab shiftwidth=4