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