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_stream_encoding(self): 373 374 """ 375 Returns the response stream encoding. 376 """ 377 378 raise NotImplementedError, "get_response_stream_encoding" 379 380 def get_response_code(self): 381 382 """ 383 Get the response code associated with the transaction. If no response 384 code is defined, None is returned. 385 """ 386 387 raise NotImplementedError, "get_response_code" 388 389 def set_response_code(self, response_code): 390 391 """ 392 Set the 'response_code' using a numeric constant defined in the HTTP 393 specification. 394 """ 395 396 raise NotImplementedError, "set_response_code" 397 398 def set_header_value(self, header, value): 399 400 """ 401 Set the HTTP 'header' with the given 'value'. 402 """ 403 404 raise NotImplementedError, "set_header_value" 405 406 def set_content_type(self, content_type): 407 408 """ 409 Sets the 'content_type' for the response. 410 """ 411 412 raise NotImplementedError, "set_content_type" 413 414 # Higher level response-related methods. 415 416 def set_cookie(self, cookie): 417 418 """ 419 Stores the given 'cookie' object in the response. 420 """ 421 422 raise NotImplementedError, "set_cookie" 423 424 def set_cookie_value(self, name, value, path=None, expires=None): 425 426 """ 427 Stores a cookie with the given 'name' and 'value' in the response. 428 429 The optional 'path' is a string which specifies the scope of the cookie, 430 and the optional 'expires' parameter is a value compatible with the 431 time.time function, and indicates the expiry date/time of the cookie. 432 """ 433 434 raise NotImplementedError, "set_cookie_value" 435 436 def delete_cookie(self, cookie_name): 437 438 """ 439 Adds to the response a request that the cookie with the given 440 'cookie_name' be deleted/discarded by the client. 441 """ 442 443 raise NotImplementedError, "delete_cookie" 444 445 # Session-related methods. 446 447 def get_session(self, create=1): 448 449 """ 450 Gets a session corresponding to an identifier supplied in the 451 transaction. 452 453 If no session has yet been established according to information 454 provided in the transaction then the optional 'create' parameter 455 determines whether a new session will be established. 456 457 Where no session has been established and where 'create' is set to 0 458 then None is returned. In all other cases, a session object is created 459 (where appropriate) and returned. 460 """ 461 462 raise NotImplementedError, "get_session" 463 464 def expire_session(self): 465 466 """ 467 Expires any session established according to information provided in the 468 transaction. 469 """ 470 471 raise NotImplementedError, "expire_session" 472 473 # Application-specific methods. 474 475 def set_user(self, username): 476 477 """ 478 An application-specific method which sets the user information with 479 'username' in the transaction. This affects subsequent calls to 480 'get_user'. 481 """ 482 483 raise NotImplementedError, "set_user" 484 485 class Resource: 486 487 "A generic resource interface." 488 489 def respond(self, trans): 490 491 """ 492 An application-specific method which performs activities on the basis of 493 the transaction object 'trans'. 494 """ 495 496 raise NotImplementedError, "respond" 497 498 class Authenticator: 499 500 "A generic authentication component." 501 502 def authenticate(self, trans): 503 504 """ 505 An application-specific method which authenticates the sender of the 506 request described by the transaction object 'trans'. This method should 507 consider 'trans' to be read-only and not attempt to change the state of 508 the transaction. 509 510 If the sender of the request is authenticated successfully, the result 511 of this method evaluates to true; otherwise the result of this method 512 evaluates to false. 513 """ 514 515 raise NotImplementedError, "authenticate" 516 517 def get_auth_type(self): 518 519 """ 520 An application-specific method which returns the authentication type to 521 be used. An example value is 'Basic' which specifies HTTP basic 522 authentication. 523 """ 524 525 raise NotImplementedError, "get_auth_type" 526 527 def get_realm(self): 528 529 """ 530 An application-specific method which returns the name of the realm for 531 which authentication is taking place. 532 """ 533 534 raise NotImplementedError, "get_realm" 535 536 # vim: tabstop=4 expandtab shiftwidth=4