1 #!/usr/bin/env python 2 3 """ 4 Generic Web framework interfaces. 5 6 Copyright (C) 2004, 2005, 2006 Paul Boddie <paul@boddie.org.uk> 7 8 This library is free software; you can redistribute it and/or 9 modify it under the terms of the GNU Lesser General Public 10 License as published by the Free Software Foundation; either 11 version 2.1 of the License, or (at your option) any later version. 12 13 This library is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 Lesser General Public License for more details. 17 18 You should have received a copy of the GNU Lesser General Public 19 License along with this library; if not, write to the Free Software 20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 21 22 -------- 23 24 The WebStack architecture consists of the following layers: 25 26 * Framework - The underlying Web framework implementation. 27 * Adapter - Code operating under the particular framework which creates 28 WebStack abstractions and issues them to the application. 29 * Resources - Units of functionality operating within the hosted Web 30 application. 31 32 Resources can act as both content producers within an application and as request 33 dispatchers to other resources; in the latter role, they may be referred to as 34 directors. 35 """ 36 37 import urllib, urlparse 38 from WebStack.Helpers.Request import Cookie, parse_header_value, ContentType, HeaderValue 39 40 class EndOfResponse(Exception): 41 42 "An exception which signals the end of a response." 43 44 pass 45 46 class Transaction: 47 48 """ 49 A generic transaction interface containing framework-specific methods to be 50 overridden. 51 """ 52 53 # The default charset ties output together with body field interpretation. 54 # It is also used to interpret URLs and paths. 55 56 default_charset = "utf-8" 57 58 # The safe default charset provides some interpretation of incoming data of 59 # an unknown encoding. Generally, one should avoid making "last resort" 60 # interpretations, however. 61 62 safe_default_charset = "iso-8859-1" 63 64 # The default path info is provided here, although the manipulated virtual 65 # path info is an instance attribute set through instances of subclasses of 66 # this class. 67 68 path_info = None 69 70 # The default user is provided here, although the manipulated user is an 71 # instance attribute set through instances of subclasses of this class. 72 73 user = None 74 75 def commit(self): 76 77 """ 78 A special method, synchronising the transaction with framework-specific 79 objects. 80 """ 81 82 pass 83 84 def rollback(self): 85 86 """ 87 A special method, partially synchronising the transaction with 88 framework-specific objects, but discarding previously emitted content 89 that is to be replaced by an error message. 90 """ 91 92 pass 93 94 # Utility methods. 95 96 def parse_header_value(self, header_class, header_value_str): 97 98 """ 99 Create an object of the given 'header_class' by determining the details 100 of the given 'header_value_str' - a string containing the value of a 101 particular header. 102 """ 103 104 # Now uses the WebStack.Helpers.Request function of the same name. 105 106 return parse_header_value(header_class, header_value_str) 107 108 def parse_content_type(self, content_type_field): 109 110 """ 111 Parse the given 'content_type_field' - a value found comparable to that 112 found in an HTTP request header for "Content-Type". 113 """ 114 115 return self.parse_header_value(ContentType, content_type_field) 116 117 def format_header_value(self, value): 118 119 """ 120 Format the given header 'value'. Typically, this just ensures the usage 121 of US-ASCII. 122 """ 123 124 return value.encode("US-ASCII") 125 126 def encode_cookie_value(self, value): 127 128 """ 129 Encode the given cookie 'value'. This ensures the usage of US-ASCII 130 through the encoding of Unicode objects as URL-encoded UTF-8 text. 131 """ 132 133 return urllib.quote(value.encode("UTF-8")).encode("US-ASCII") 134 135 def decode_cookie_value(self, value): 136 137 """ 138 Decode the given cookie 'value'. 139 """ 140 141 return unicode(urllib.unquote(value), "UTF-8") 142 143 def process_cookies(self, cookie_dict, using_strings=0): 144 145 """ 146 Process the given 'cookie_dict', returning a dictionary mapping cookie names 147 to cookie objects where the names and values have been decoded from the form 148 used in the cookies retrieved from the request. 149 150 The optional 'using_strings', if set to 1, treats the 'cookie_dict' as a 151 mapping of cookie names to values. 152 """ 153 154 cookies = {} 155 for name in cookie_dict.keys(): 156 if using_strings: 157 value = cookie_dict[name] 158 else: 159 cookie = cookie_dict[name] 160 value = cookie.value 161 cookie_name = self.decode_cookie_value(name) 162 cookie_value = self.decode_cookie_value(value) 163 cookies[cookie_name] = Cookie(cookie_name, cookie_value) 164 return cookies 165 166 def parse_content_preferences(self, accept_preference): 167 168 """ 169 Returns the preferences as requested by the user agent. The preferences are 170 returned as a list of codes in the same order as they appeared in the 171 appropriate environment variable. In other words, the explicit weighting 172 criteria are ignored. 173 174 As the 'accept_preference' parameter, values for language and charset 175 preferences are appropriate. 176 """ 177 178 if accept_preference is None: 179 return [] 180 181 accept_defs = accept_preference.split(",") 182 accept_prefs = [] 183 for accept_def in accept_defs: 184 t = accept_def.split(";") 185 if len(t) >= 1: 186 accept_prefs.append(t[0].strip()) 187 return accept_prefs 188 189 def convert_to_list(self, value): 190 191 """ 192 Returns a single element list containing 'value' if it is not itself a list, a 193 tuple, or None. If 'value' is a list then it is itself returned; if 'value' is a 194 tuple then a new list containing the same elements is returned; if 'value' is None 195 then an empty list is returned. 196 """ 197 198 if type(value) == type([]): 199 return value 200 elif type(value) == type(()): 201 return list(value) 202 elif value is None: 203 return [] 204 else: 205 return [value] 206 207 # Public utility methods. 208 209 def decode_path(self, path, encoding=None): 210 211 """ 212 From the given 'path', use the optional 'encoding' (if specified) to decode the 213 information and convert it to Unicode. Upon failure for a specified 'encoding' 214 or where 'encoding' is not specified, use the default character encoding to 215 perform the conversion. 216 217 Returns the 'path' as a Unicode value without "URL encoded" character values. 218 """ 219 220 unquoted_path = urllib.unquote(path) 221 return self.decode_value(unquoted_path, encoding) 222 223 def decode_value(self, value, encoding=None): 224 225 """ 226 From the given 'value', use the optional 'encoding' (if specified) to decode the 227 information and convert it to Unicode. Upon failure for a specified 'encoding' 228 or where 'encoding' is not specified, use the default character encoding to 229 perform the conversion. 230 231 Returns the 'value' as a Unicode value. 232 """ 233 234 if encoding is not None: 235 try: 236 return unicode(value, encoding) 237 except UnicodeError: 238 pass 239 try: 240 return unicode(value, self.default_charset) 241 except UnicodeError: 242 return unicode(value, self.safe_default_charset) 243 244 def encode_url_without_query(self, url, encoding=None): 245 246 """ 247 Encode the given full 'url', using the optional 'encoding' (if specified) or the 248 default encoding where 'encoding' is not specified, and produce a suitable encoded 249 URL. This method effectively performs URL-encoding on the path of 'url', leaving 250 the protocol scheme and host information intact. 251 """ 252 253 scheme, netloc, path, query, fragment = urlparse.urlsplit(url) 254 return scheme + "://" + netloc + self.encode_path(path, encoding) 255 256 def encode_path(self, path, encoding=None): 257 258 """ 259 Encode the given 'path', using the optional 'encoding' (if specified) or the 260 default encoding where 'encoding' is not specified, and produce a suitable "URL 261 encoded" string. 262 """ 263 264 return urllib.quote(self.encode_value(path, encoding)) 265 266 def encode_value(self, value, encoding=None): 267 268 """ 269 Encode the given 'value', using the optional 'encoding' (if specified) or the 270 default encoding where 'encoding' is not specified, producing a plain string. 271 """ 272 273 if encoding is not None: 274 return value.encode(encoding) 275 else: 276 try: 277 return value.encode(self.default_charset) 278 except UnicodeError: 279 return value.encode(self.safe_default_charset) 280 281 # Server-related methods. 282 283 def get_server_name(self): 284 285 "Returns the server name." 286 287 raise NotImplementedError, "get_server_name" 288 289 def get_server_port(self): 290 291 "Returns the server port as a string." 292 293 raise NotImplementedError, "get_server_port" 294 295 # Request-related methods. 296 297 def get_request_stream(self): 298 299 """ 300 Returns the request stream for the transaction. 301 """ 302 303 raise NotImplementedError, "get_request_stream" 304 305 def get_request_method(self): 306 307 """ 308 Returns the request method. 309 """ 310 311 raise NotImplementedError, "get_request_method" 312 313 def get_headers(self): 314 315 """ 316 Returns all request headers as a dictionary-like object mapping header 317 names to values. 318 """ 319 320 raise NotImplementedError, "get_headers" 321 322 def get_header_values(self, key): 323 324 """ 325 Returns a list of all request header values associated with the given 326 'key'. Note that according to RFC 2616, 'key' is treated as a 327 case-insensitive string. 328 """ 329 330 raise NotImplementedError, "get_header_values" 331 332 def get_content_type(self): 333 334 """ 335 Returns the content type specified on the request, along with the 336 charset employed. 337 """ 338 339 raise NotImplementedError, "get_content_type" 340 341 def get_content_charsets(self): 342 343 """ 344 Returns the character set preferences. 345 """ 346 347 raise NotImplementedError, "get_content_charsets" 348 349 def get_content_languages(self): 350 351 """ 352 Returns extracted language information from the transaction. 353 """ 354 355 raise NotImplementedError, "get_content_languages" 356 357 def get_path(self, encoding=None): 358 359 """ 360 Returns the entire path from the request as a Unicode object. Any "URL 361 encoded" character values in the part of the path before the query 362 string will be decoded and presented as genuine characters; the query 363 string will remain "URL encoded", however. 364 365 If the optional 'encoding' is set, use that in preference to the default 366 encoding to convert the path into a form not containing "URL encoded" 367 character values. 368 """ 369 370 raise NotImplementedError, "get_path" 371 372 def get_path_without_query(self, encoding=None): 373 374 """ 375 Returns the entire path from the request minus the query string as a 376 Unicode object containing genuine characters (as opposed to "URL 377 encoded" character values). 378 379 If the optional 'encoding' is set, use that in preference to the default 380 encoding to convert the path into a form not containing "URL encoded" 381 character values. 382 """ 383 384 raise NotImplementedError, "get_path_without_query" 385 386 def get_path_info(self, encoding=None): 387 388 """ 389 Returns the "path info" (the part of the URL after the resource name 390 handling the current request) from the request as a Unicode object 391 containing genuine characters (as opposed to "URL encoded" character 392 values). 393 394 If the optional 'encoding' is set, use that in preference to the default 395 encoding to convert the path into a form not containing "URL encoded" 396 character values. 397 """ 398 399 raise NotImplementedError, "get_path_info" 400 401 def get_path_without_info(self, encoding=None): 402 403 """ 404 Returns the entire path from the request minus the query string and the 405 "path info" as a Unicode object containing genuine characters (as 406 opposed to "URL encoded" character values). 407 408 If the optional 'encoding' is set, use that in preference to the default 409 encoding to convert the path into a form not containing "URL encoded" 410 character values. 411 """ 412 413 entire_path = self.get_path_without_query(encoding) 414 path_info = self.get_path_info(encoding) 415 return entire_path[:-len(path_info)] 416 417 def get_query_string(self): 418 419 """ 420 Returns the query string from the path in the request. 421 """ 422 423 raise NotImplementedError, "get_query_string" 424 425 # Higher level request-related methods. 426 427 def get_fields_from_path(self, encoding=None): 428 429 """ 430 Extracts fields (or request parameters) from the path specified in the 431 transaction. The underlying framework may refuse to supply fields from 432 the path if handling a POST transaction. The optional 'encoding' 433 parameter specifies the character encoding of the query string for cases 434 where the default encoding is to be overridden. 435 436 Returns a dictionary mapping field names to lists of values (even if a 437 single value is associated with any given field name). 438 """ 439 440 raise NotImplementedError, "get_fields_from_path" 441 442 def get_fields_from_body(self, encoding=None): 443 444 """ 445 Extracts fields (or request parameters) from the message body in the 446 transaction. The optional 'encoding' parameter specifies the character 447 encoding of the message body for cases where no such information is 448 available, but where the default encoding is to be overridden. 449 450 Returns a dictionary mapping field names to lists of values (even if a 451 single value is associated with any given field name). Each value is 452 either a Unicode object (representing a simple form field, for example) 453 or a WebStack.Helpers.Request.FileContent object (representing a file 454 upload form field). 455 """ 456 457 raise NotImplementedError, "get_fields_from_body" 458 459 def get_fields(self, encoding=None): 460 461 """ 462 Extracts fields (or request parameters) from both the path specified in 463 the transaction as well as the message body. The optional 'encoding' 464 parameter specifies the character encoding of the message body for cases 465 where no such information is available, but where the default encoding 466 is to be overridden. 467 468 Returns a dictionary mapping field names to lists of values (even if a 469 single value is associated with any given field name). Each value is 470 either a Unicode object (representing a simple form field, for example) 471 or a WebStack.Helpers.Request.FileContent object (representing a file 472 upload form field). 473 474 Where a given field name is used in both the path and message body to 475 specify values, the values from both sources will be combined into a 476 single list associated with that field name. 477 """ 478 479 raise NotImplementedError, "get_fields" 480 481 def get_user(self): 482 483 """ 484 Extracts user information from the transaction. 485 486 Returns a username as a string or None if no user is defined. 487 """ 488 489 raise NotImplementedError, "get_user" 490 491 def get_cookies(self): 492 493 """ 494 Obtains cookie information from the request. 495 496 Returns a dictionary mapping cookie names to cookie objects. 497 """ 498 499 raise NotImplementedError, "get_cookies" 500 501 def get_cookie(self, cookie_name): 502 503 """ 504 Obtains cookie information from the request. 505 506 Returns a cookie object for the given 'cookie_name' or None if no such 507 cookie exists. 508 """ 509 510 raise NotImplementedError, "get_cookie" 511 512 # Response-related methods. 513 514 def get_response_stream(self): 515 516 """ 517 Returns the response stream for the transaction. 518 """ 519 520 raise NotImplementedError, "get_response_stream" 521 522 def get_response_stream_encoding(self): 523 524 """ 525 Returns the response stream encoding. 526 """ 527 528 raise NotImplementedError, "get_response_stream_encoding" 529 530 def get_response_code(self): 531 532 """ 533 Get the response code associated with the transaction. If no response 534 code is defined, None is returned. 535 """ 536 537 raise NotImplementedError, "get_response_code" 538 539 def set_response_code(self, response_code): 540 541 """ 542 Set the 'response_code' using a numeric constant defined in the HTTP 543 specification. 544 """ 545 546 raise NotImplementedError, "set_response_code" 547 548 def set_header_value(self, header, value): 549 550 """ 551 Set the HTTP 'header' with the given 'value'. 552 """ 553 554 raise NotImplementedError, "set_header_value" 555 556 def set_content_type(self, content_type): 557 558 """ 559 Sets the 'content_type' for the response. 560 """ 561 562 raise NotImplementedError, "set_content_type" 563 564 # Higher level response-related methods. 565 566 def set_cookie(self, cookie): 567 568 """ 569 Stores the given 'cookie' object in the response. 570 """ 571 572 raise NotImplementedError, "set_cookie" 573 574 def set_cookie_value(self, name, value, path=None, expires=None): 575 576 """ 577 Stores a cookie with the given 'name' and 'value' in the response. 578 579 The optional 'path' is a string which specifies the scope of the cookie, 580 and the optional 'expires' parameter is a value compatible with the 581 time.time function, and indicates the expiry date/time of the cookie. 582 """ 583 584 raise NotImplementedError, "set_cookie_value" 585 586 def delete_cookie(self, cookie_name): 587 588 """ 589 Adds to the response a request that the cookie with the given 590 'cookie_name' be deleted/discarded by the client. 591 """ 592 593 raise NotImplementedError, "delete_cookie" 594 595 # Session-related methods. 596 597 def get_session(self, create=1): 598 599 """ 600 Gets a session corresponding to an identifier supplied in the 601 transaction. 602 603 If no session has yet been established according to information 604 provided in the transaction then the optional 'create' parameter 605 determines whether a new session will be established. 606 607 Where no session has been established and where 'create' is set to 0 608 then None is returned. In all other cases, a session object is created 609 (where appropriate) and returned. 610 """ 611 612 raise NotImplementedError, "get_session" 613 614 def expire_session(self): 615 616 """ 617 Expires any session established according to information provided in the 618 transaction. 619 """ 620 621 raise NotImplementedError, "expire_session" 622 623 # Application-specific methods. 624 625 def set_user(self, username): 626 627 """ 628 An application-specific method which sets the user information with 629 'username' in the transaction. This affects subsequent calls to 630 'get_user'. 631 """ 632 633 self.user = username 634 635 def set_virtual_path_info(self, path_info): 636 637 """ 638 An application-specific method which sets the 'path_info' in the 639 transaction. This affects subsequent calls to 'get_virtual_path_info'. 640 641 Note that the virtual path info should either be an empty string, or it 642 should begin with "/" and then (optionally) include other details. 643 Virtual path info strings which omit the leading "/" - ie. containing 644 things like "xxx" or even "xxx/yyy" - do not really make sense and may 645 not be handled correctly by various WebStack components. 646 """ 647 648 self.path_info = path_info 649 650 def get_virtual_path_info(self, encoding=None): 651 652 """ 653 An application-specific method which either returns path info set in the 654 'set_virtual_path_info' method, or the normal path info found in the 655 request. 656 657 If the optional 'encoding' is set, use that in preference to the default 658 encoding to convert the path into a form not containing "URL encoded" 659 character values. 660 """ 661 662 if self.path_info is not None: 663 return self.path_info 664 else: 665 return self.get_path_info(encoding) 666 667 def get_processed_virtual_path_info(self, encoding=None): 668 669 """ 670 An application-specific method which returns the virtual path info that 671 is considered "processed"; that is, the part of the path info which is 672 not included in the virtual path info. 673 674 If the optional 'encoding' is set, use that in preference to the default 675 encoding to convert the path into a form not containing "URL encoded" 676 character values. 677 678 Where the virtual path info is identical to the path info, an empty 679 string is returned. 680 681 Where the virtual path info is a substring of the path info, the path 682 info preceding that substring is returned. 683 684 Where the virtual path info is either an empty string or not a substring 685 of the path info, the entire path info is returned. 686 687 Generally, one should expect the following relationship between the path 688 info, virtual path info and processed virtual path info: 689 690 path info == processed virtual path info + virtual path info 691 """ 692 693 real_path_info = self.get_path_info(encoding) 694 virtual_path_info = self.get_virtual_path_info(encoding) 695 696 if virtual_path_info == "": 697 return real_path_info 698 699 i = real_path_info.rfind(virtual_path_info) 700 if i == -1: 701 return real_path_info 702 else: 703 return real_path_info[:i] 704 705 def get_attributes(self): 706 707 """ 708 An application-specific method which obtains a dictionary mapping names 709 to attribute values that can be used to store arbitrary information. 710 711 Since the dictionary of attributes is retained by the transaction during 712 its lifetime, such a dictionary can be used to store information that an 713 application wishes to communicate amongst its components and resources 714 without having to pass objects other than the transaction between them. 715 716 The returned dictionary can be modified using normal dictionary-like 717 methods. If no attributes existed previously, a new dictionary is 718 created and associated with the transaction. 719 """ 720 721 if not hasattr(self, "_attributes"): 722 self._attributes = {} 723 return self._attributes 724 725 # Utility methods. 726 727 def traverse_path(self, encoding=None): 728 729 """ 730 Traverse the path, updating the virtual path info and thus the processed 731 virtual path info accordingly. Return the traversed virtual path info 732 fragment. 733 """ 734 735 vp = self.get_virtual_path_info(encoding).split("/") 736 self.set_virtual_path_info("/" + "/".join(vp[2:])) 737 return vp[1] 738 739 def update_path(self, path, relative_path): 740 741 """ 742 Transform the given 'path' using the specified 'relative_path'. For 743 example, a simple identifier replaces the last component from 'path': 744 745 trans.update_path("/parent/node", "other") -> "/parent/other" 746 747 If the last component is empty, the effect is similar to an append 748 operation: 749 750 trans.update_path("/parent/node/", "other") -> "/parent/node/other" 751 752 Where 'relative_path' is empty, the result is 'path' with the last 753 component erased (but still present): 754 755 trans.update_path("/parent/node", "") -> "/parent/" 756 757 trans.update_path("/parent/node/", "") -> "/parent/node/" 758 759 Where 'relative_path' contains ".", the component is regarded as being 760 empty: 761 762 trans.update_path("/parent/node", "other/./more") -> "/parent/other/more" 763 764 trans.update_path("/parent/node/", "other/./more") -> "/parent/node/other/more" 765 766 However, at the start of 'relative_path', "." can remove one component: 767 768 trans.update_path("/parent/node", ".") -> "/parent" 769 770 trans.update_path("/parent/node/", ".") -> "/parent/node" 771 772 Adding "/" immediately afterwards restores any removed "/": 773 774 trans.update_path("/parent/node/", "./") -> "/parent/node/" 775 776 trans.update_path("/parent/node", "./") -> "/parent/" 777 778 Following components add to the effect of "./": 779 780 trans.update_path("/parent/node", "./other/more") -> "/parent/other/more" 781 782 trans.update_path("/parent/node/", "./other/more") -> "/parent/node/other/more" 783 784 Where 'relative_path' contains "..", two components are removed from the 785 resulting path: 786 787 trans.update_path("/parent/node/", "..") -> "/parent" 788 789 trans.update_path("/parent/node/", "../other") -> "/parent/other" 790 791 trans.update_path("/parent/node", "..") -> "/" 792 793 trans.update_path("/parent/node", "../other") -> "/other" 794 795 Where fewer components exist than are to be removed, the path is reset: 796 797 trans.update_path("/parent/node", "../..") -> "/" 798 799 Subsequent components are applied to the reset path: 800 801 trans.update_path("/parent/node", "../../other") -> "/other" 802 803 trans.update_path("/parent/node/", "../../other") -> "/other" 804 805 Where 'relative_path' begins with "/", the 'path' is reset to "/" and 806 the components of the 'relative_path' are then applied to that new path: 807 808 trans.update_path("/parent/node", "/other") -> "/other" 809 810 Where 'relative_path' ends with "/", the final "/" is added to the 811 result: 812 813 trans.update_path("/parent/node", "other/") -> "/parent/other/" 814 """ 815 816 rparts = relative_path.split("/") 817 818 if relative_path.startswith("/"): 819 parts = [""] 820 del rparts[0] 821 elif relative_path == "": 822 parts = path.split("/") 823 parts[-1] = "" 824 del rparts[0] 825 else: 826 parts = path.split("/") 827 del parts[-1] 828 829 for rpart in rparts: 830 if rpart == ".": 831 continue 832 elif rpart == "..": 833 if len(parts) > 1: 834 parts = parts[:-1] 835 else: 836 parts.append(rpart) 837 838 return "/" + "/".join(parts[1:]) 839 840 def redirect(self, path, code=302): 841 842 """ 843 Send a redirect response to the client, providing the given 'path' as 844 the suggested location of a resource. The optional 'code' (set to 302 by 845 default) may be used to change the exact meaning of the response 846 according to the HTTP specifications. 847 848 Note that 'path' should be a plain string suitable for header output. 849 Use the 'encode_path' method to convert Unicode objects into such 850 strings. 851 """ 852 853 self.set_response_code(code) 854 self.set_header_value("Location", path) 855 raise EndOfResponse 856 857 class Resource: 858 859 "A generic resource interface." 860 861 def respond(self, trans): 862 863 """ 864 An application-specific method which performs activities on the basis of 865 the transaction object 'trans'. 866 """ 867 868 raise NotImplementedError, "respond" 869 870 class Authenticator: 871 872 "A generic authentication component." 873 874 def authenticate(self, trans): 875 876 """ 877 An application-specific method which authenticates the sender of the 878 request described by the transaction object 'trans'. This method should 879 consider 'trans' to be read-only and not attempt to change the state of 880 the transaction. 881 882 If the sender of the request is authenticated successfully, the result 883 of this method evaluates to true; otherwise the result of this method 884 evaluates to false. 885 """ 886 887 raise NotImplementedError, "authenticate" 888 889 def get_auth_type(self): 890 891 """ 892 An application-specific method which returns the authentication type to 893 be used. An example value is 'Basic' which specifies HTTP basic 894 authentication. 895 """ 896 897 raise NotImplementedError, "get_auth_type" 898 899 def get_realm(self): 900 901 """ 902 An application-specific method which returns the name of the realm for 903 which authentication is taking place. 904 """ 905 906 raise NotImplementedError, "get_realm" 907 908 # vim: tabstop=4 expandtab shiftwidth=4