Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Changeset 790

Show
Ignore:
Timestamp:
11/05/05 01:17:13
Author:
fumanchu
Message:

Moved a bunch of stuff from lib/cptools into a new lib/httptools module. The new module does not reference cherrypy (and should never do so). Think of it as the httptools module Python should distribute. ;)

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/cherrypy/_cphttptools.py

    r788 r790  
    1010import cherrypy 
    1111from cherrypy import _cputil, _cpcgifs, _cpwsgiserver 
    12 from cherrypy.lib import cptools 
    13  
    14  
    15 class Version(object): 
    16      
    17     """A version, such as "2.1 beta 3", which can be compared atom-by-atom. 
    18      
    19     If a string is provided to the constructor, it will be split on word 
    20     boundaries; that is, "1.4.13 beta 9" -> ["1", "4", "13", "beta", "9"]. 
    21      
    22     Comparisons are performed atom-by-atom, numerically if both atoms are 
    23     numeric. Therefore, "2.12" is greater than "2.4", and "3.0 beta" is 
    24     greater than "3.0 alpha" (only because "b" > "a"). If an atom is 
    25     provided in one Version and not another, the longer Version is 
    26     greater than the shorter, that is: "4.8 alpha" > "4.8". 
    27     """ 
    28      
    29     def __init__(self, atoms): 
    30         """A Version object. A str argument will be split on word boundaries.""" 
    31         if isinstance(atoms, basestring): 
    32             self.atoms = re.split(r'\W', atoms) 
    33         else: 
    34             self.atoms = [str(x) for x in atoms] 
    35      
    36     def from_http(cls, version_str): 
    37         """Return a Version object from the given 'HTTP/x.y' string.""" 
    38         return cls(version_str[5:]) 
    39     from_http = classmethod(from_http) 
    40      
    41     def to_http(self): 
    42         """Return a 'HTTP/x.y' string for this Version object.""" 
    43         return "HTTP/%s.%s" % tuple(self.atoms[:2]) 
    44      
    45     def __str__(self): 
    46         return ".".join([str(x) for x in self.atoms]) 
    47      
    48     def __cmp__(self, other): 
    49         cls = self.__class__ 
    50         if not isinstance(other, cls): 
    51             # Try to coerce other to a Version instance. 
    52             other = cls(other) 
    53          
    54         index = 0 
    55         while index < len(self.atoms) and index < len(other.atoms): 
    56             mine, theirs = self.atoms[index], other.atoms[index] 
    57             if mine.isdigit() and theirs.isdigit(): 
    58                 mine, theirs = int(mine), int(theirs) 
    59             if mine < theirs: 
    60                 return -1 
    61             if mine > theirs: 
    62                 return 1 
    63             index += 1 
    64         if index < len(other.atoms): 
    65             return -1 
    66         if index < len(self.atoms): 
    67             return 1 
    68         return 0 
     12from cherrypy.lib import cptools, httptools 
    6913 
    7014 
     
    11256        try: 
    11357            self.headers = headers 
    114             self.headerMap = cptools.HeaderMap() 
     58            self.headerMap = httptools.HeaderMap() 
    11559            self.simpleCookie = Cookie.SimpleCookie() 
    11660            self.rfile = rfile 
     
    161105    def processRequestLine(self, requestLine): 
    162106        self.requestLine = rl = requestLine.strip() 
    163         method, path, qs, proto = cptools.parseRequestLine(rl) 
     107        method, path, qs, proto = httptools.parseRequestLine(rl) 
    164108        if path == "*": 
    165109            path = "global" 
     
    192136         
    193137        # cherrypy.request.version == request.protocol in a Version instance. 
    194         self.version = Version.from_http(self.protocol) 
     138        self.version = httptools.Version.from_http(self.protocol) 
    195139        server_v = cherrypy.config.get("server.protocolVersion", "HTTP/1.0") 
    196         server_v = Version.from_http(server_v) 
     140        server_v = httptools.Version.from_http(server_v) 
    197141         
    198142        # cherrypy.response.version should be used to determine whether or 
     
    200144        cherrypy.response.version = min(self.version, server_v) 
    201145         
    202         self.paramMap = cptools.parseQueryString(self.queryString) 
     146        self.paramMap = httptools.parseQueryString(self.queryString) 
    203147         
    204148        # Process the headers into self.headerMap 
     
    252196            self.body = forms.file 
    253197        else: 
    254             self.paramMap.update(cptools.paramsFromCGIForm(forms)) 
     198            self.paramMap.update(httptools.paramsFromCGIForm(forms)) 
    255199     
    256200    def main(self, path=None): 
     
    361305        self.body = None 
    362306         
    363         self.headerMap = cptools.HeaderMap() 
     307        self.headerMap = httptools.HeaderMap() 
    364308        self.headerMap.update({ 
    365309            "Content-Type": "text/html", 
    366310            "Server": "CherryPy/" + cherrypy.__version__, 
    367             "Date": cptools.HTTPDate(), 
     311            "Date": httptools.HTTPDate(), 
    368312            "Set-Cookie": [], 
    369313            "Content-Length": None 
     
    374318        """Transform headerMap (and cookies) into cherrypy.response.headers.""" 
    375319         
    376         code, reason, _ = cptools.validStatus(self.status) 
     320        try: 
     321            code, reason, _ = httptools.validStatus(self.status) 
     322        except ValueError, x: 
     323            raise cherrypy.HTTPError(500, x.args[0]) 
     324         
    377325        self.status = "%s %s" % (code, reason) 
    378326         
  • trunk/cherrypy/_cputil.py

    r788 r790  
    77 
    88import cherrypy 
    9 from cherrypy.lib import cptools 
     9from cherrypy.lib import httptools 
    1010 
    1111 
     
    156156    """ 
    157157     
    158     code, reason, message = cptools.validStatus(status) 
     158    try: 
     159        code, reason, message = httptools.validStatus(status) 
     160    except ValueError, x: 
     161        raise cherrypy.HTTPError(500, x.args[0]) 
    159162     
    160163    # We can't use setdefault here, because some 
  • trunk/cherrypy/lib/cptools.py

    r775 r790  
    11"""Tools which both CherryPy and application developers may invoke.""" 
    22 
    3 from BaseHTTPServer import BaseHTTPRequestHandler 
    4 responseCodes = BaseHTTPRequestHandler.responses.copy() 
    5  
    6 import cgi 
    73import inspect 
    84import mimetools 
    9  
    105import mimetypes 
    116mimetypes.types_map['.dwg']='image/x-dwg' 
     
    138 
    149import os 
    15 import re 
    1610import sys 
    1711import time 
    18 import urllib 
    19 from urlparse import urlparse 
    2012 
    2113import cherrypy 
     14import httptools 
    2215 
    2316 
     
    6962 
    7063 
    71 class PositionalParametersAware(object): 
    72     """ 
    73     Utility class that restores positional parameters functionality that 
    74     was found in 2.0.0-beta. 
    75  
    76     Use case: 
    77  
    78     from cherrypy.lib import cptools 
    79     import cherrypy 
    80     class Root(cptools.PositionalParametersAware): 
    81         def something(self, name): 
    82             return "hello, " + name 
    83         something.exposed 
    84     cherrypy.root = Root() 
    85     cherrypy.server.start() 
    86  
    87     Now, fetch http://localhost:8080/something/name_is_here 
    88     """ 
    89     def default( self, *args, **kwargs ): 
    90         # remap parameters to fix positional parameters 
    91         if len(args) == 0: 
    92             args = ("index",) 
    93         m = getattr(self, args[0], None) 
    94         if m and getattr(m, "exposed", False): 
    95             return getattr(self, args[0])(*args[1:], **kwargs) 
    96         else: 
    97             m = getattr(self, "index", None) 
    98             if m and getattr(m, "exposed", False): 
    99                 try: 
    100                     return self.index(*args, **kwargs) 
    101                 except TypeError: 
    102                     pass 
    103             raise cherrypy.NotFound() 
    104     default.exposed = True 
    105  
    106  
    107 weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 
    108 monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 
    109                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 
    110  
    111 def HTTPDate(dt=None): 
    112     """Return the given time.struct_time as a string in RFC 1123 format. 
    113      
    114     If no arguments are provided, the current time (as determined by 
    115     time.gmtime() is used). 
    116      
    117     RFC 2616: "[Concerning RFC 1123, RFC 850, asctime date formats]... 
    118     HTTP/1.1 clients and servers that parse the date value MUST 
    119     accept all three formats (for compatibility with HTTP/1.0), 
    120     though they MUST only generate the RFC 1123 format for 
    121     representing HTTP-date values in header fields." 
    122      
    123     RFC 1945 (HTTP/1.0) requires the same. 
    124      
    125     """ 
    126      
    127     if dt is None: 
    128         dt = time.gmtime() 
    129      
    130     year, month, day, hh, mm, ss, wd, y, z = dt 
    131     # Is "%a, %d %b %Y %H:%M:%S GMT" better or worse? 
    132     return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" % 
    133             (weekdayname[wd], day, monthname[month], year, hh, mm, ss)) 
    134  
    135  
    136 def getRanges(content_length): 
    137     """Return a list of (start, stop) indices from a Range header, or None. 
    138      
    139     Each (start, stop) tuple will be composed of two ints, which are suitable 
    140     for use in a slicing operation. That is, the header "Range: bytes=3-6", 
    141     if applied against a Python string, is requesting resource[3:7]. This 
    142     function will return the list [(3, 7)]. 
    143     """ 
    144      
    145     r = cherrypy.request.headerMap.get('Range') 
    146     if not r: 
    147         return None 
    148      
    149     result = [] 
    150     bytesunit, byteranges = r.split("=", 1) 
    151     for brange in byteranges.split(","): 
    152         start, stop = [x.strip() for x in brange.split("-", 1)] 
    153         if start: 
    154             if not stop: 
    155                 stop = content_length - 1 
    156             start, stop = map(int, (start, stop)) 
    157             if start >= content_length: 
    158                 # From rfc 2616 sec 14.16: 
    159                 # "If the server receives a request (other than one 
    160                 # including an If-Range request-header field) with an 
    161                 # unsatisfiable Range request-header field (that is, 
    162                 # all of whose byte-range-spec values have a first-byte-pos 
    163                 # value greater than the current length of the selected 
    164                 # resource), it SHOULD return a response code of 416 
    165                 # (Requested range not satisfiable)." 
    166                 continue 
    167             if stop < start: 
    168                 # From rfc 2616 sec 14.16: 
    169                 # "If the server ignores a byte-range-spec because it 
    170                 # is syntactically invalid, the server SHOULD treat 
    171                 # the request as if the invalid Range header field 
    172                 # did not exist. (Normally, this means return a 200 
    173                 # response containing the full entity)." 
    174                 return None 
    175             result.append((start, stop + 1)) 
    176         else: 
    177             if not stop: 
    178                 # See rfc quote above. 
    179                 return None 
    180             # Negative subscript (last N bytes) 
    181             result.append((content_length - int(stop), content_length)) 
    182      
    183     if result == []: 
    184         cherrypy.response.headerMap['Content-Range'] = "bytes */%s" % content_length 
    185         message = "Invalid Range (first-byte-pos greater than Content-Length)" 
    186         raise cherrypy.HTTPError(416, message) 
    187      
    188     return result 
    189  
    190  
    191 class AcceptValue(object): 
    192     """A value (with parameters) from an Accept-* request header.""" 
    193      
    194     def __init__(self, value, params=None): 
    195         self.value = value 
    196         if params is None: 
    197             params = {} 
    198         self.params = params 
    199      
    200     def qvalue(self): 
    201         val = self.params.get("q", "1") 
    202         if isinstance(val, AcceptValue): 
    203             val = val.value 
    204         return float(val) 
    205     qvalue = property(qvalue, doc="The qvalue, or priority, of this value.") 
    206      
    207     def __str__(self): 
    208         p = [";%s=%s" % (k, v) for k, v in self.params.iteritems()] 
    209         return "%s%s" % (self.value, "".join(p)) 
    210      
    211     def __cmp__(self, other): 
    212         # If you sort a list of AcceptValue objects, they will be listed in 
    213         # priority order; that is, the most preferred value will be first. 
    214         diff = cmp(other.qvalue, self.qvalue) 
    215         if diff == 0: 
    216             diff = cmp(str(other), str(self)) 
    217         return diff 
    218  
    219  
    220 def getAccept(headername='Accept'): 
    221     """Return a list of AcceptValues from an Accept header, or None.""" 
    222      
    223     r = cherrypy.request.headerMap.get(headername) 
    224     if not r: 
    225         return None 
    226      
    227     result = [] 
    228     for capability in r.split(","): 
    229         # The first "q" parameter (if any) separates the initial 
    230         # parameter(s) (if any) from the accept-params. 
    231         atoms = re.split(r'; *q *=', capability, 1) 
    232         capvalue = atoms.pop(0).strip() 
    233         if atoms: 
    234             qvalue = atoms[0].strip() 
    235             if headername == 'Accept': 
    236                 # The qvalue for an Accept header can have extensions. 
    237                 atoms = [x.strip() for x in qvalue.split(";")] 
    238                 qvalue = atoms.pop(0).strip() 
    239                 ext = {} 
    240                 for atom in atoms: 
    241                     atom = atom.split("=", 1) 
    242                     key = atom.pop(0).strip() 
    243                     if atom: 
    244                         val = atom[0].strip() 
    245                     else: 
    246                         val = "" 
    247                     ext[key] = val 
    248                 qvalue = AcceptValue(qvalue, ext) 
    249             params = {"q": qvalue} 
    250         else: 
    251             params = {} 
    252          
    253         if headername == 'Accept': 
    254             # The media-range may have parameters (before the qvalue). 
    255             atoms = [x.strip() for x in capvalue.split(";")] 
    256             capvalue = atoms.pop(0).strip() 
    257             for atom in atoms: 
    258                 atom = atom.split("=", 1) 
    259                 key = atom.pop(0).strip() 
    260                 if atom: 
    261                     val = atom[0].strip() 
    262                 else: 
    263                     val = "" 
    264                 params[key] = val 
    265          
    266         result.append(AcceptValue(capvalue, params)) 
    267      
    268     result.sort() 
    269     return result 
    270  
    271  
    27264def serveFile(path, contentType=None, disposition=None, name=None): 
    27365    """Set status, headers, and body in order to serve the given file. 
     
    312104    response.headerMap['Content-Type'] = contentType 
    313105     
    314     strModifTime = HTTPDate(time.gmtime(stat.st_mtime)) 
     106    strModifTime = httptools.HTTPDate(time.gmtime(stat.st_mtime)) 
    315107    if cherrypy.request.headerMap.has_key('If-Modified-Since'): 
    316108        if cherrypy.request.headerMap['If-Modified-Since'] == strModifTime: 
     
    338130    if cherrypy.response.version >= "1.1": 
    339131        response.headerMap["Accept-Ranges"] = "bytes" 
    340         r = getRanges(c_len) 
     132        r = httptools.getRanges(cherrypy.request.headerMap.get('Range'), c_len) 
     133        if r == []: 
     134            response.headerMap['Content-Range'] = "bytes */%s" % c_len 
     135            message = "Invalid Range (first-byte-pos greater than Content-Length)" 
     136            raise cherrypy.HTTPError(416, message) 
    341137        if r: 
    342138            if len(r) == 1: 
     
    385181        chunk = input.read(chunkSize) 
    386182    input.close() 
    387  
    388 def validStatus(status): 
    389     """Return legal HTTP status Code, Reason-phrase and Message. 
    390      
    391     The status arg must be an int, or a str that begins with an int. 
    392      
    393     If status is an int, or a str and  no reason-phrase is supplied, 
    394     a default reason-phrase will be provided. 
    395     """ 
    396      
    397     if not status: 
    398         status = 200 
    399      
    400     status = str(status) 
    401     parts = status.split(" ", 1) 
    402     if len(parts) == 1: 
    403         # No reason supplied. 
    404         code, = parts 
    405         reason = None 
    406     else: 
    407         code, reason = parts 
    408         reason = reason.strip() 
    409      
    410     try: 
    411         code = int(code) 
    412     except ValueError: 
    413         raise cherrypy.HTTPError(500, "Illegal response status from server (non-numeric).") 
    414      
    415     if code < 100 or code > 599: 
    416         raise cherrypy.HTTPError(500, "Illegal response status from server (out of range).") 
    417      
    418     if code not in responseCodes: 
    419         # code is unknown but not illegal 
    420         defaultReason, message = "", "" 
    421     else: 
    422         defaultReason, message = responseCodes[code] 
    423      
    424     if reason is None: 
    425         reason = defaultReason 
    426      
    427     return code, reason, message 
    428  
    429 def parseRequestLine(requestLine): 
    430     """Return (method, path, querystring, protocol) from a requestLine.""" 
    431     method, path, protocol = requestLine.split() 
    432      
    433     # path may be an abs_path (including "http://host.domain.tld"); 
    434     # Ignore scheme, location, and fragments (so config lookups work). 
    435     # [Therefore, this assumes all hosts are valid for this server.] 
    436     scheme, location, path, params, qs, frag = urlparse(path) 
    437     if path == "*": 
    438         # "...the request does not apply to a particular resource, 
    439         # but to the server itself". See 
    440         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 
    441         pass 
    442     else: 
    443         if params: 
    444             params = ";" + params 
    445         path = path + params 
    446          
    447         # Unquote the path (e.g. "/this%20path" -> "this path"). 
    448         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 
    449         # Note that cgi.parse_qs will decode the querystring for us. 
    450         path = urllib.unquote(path) 
    451      
    452     return method, path, qs, protocol 
    453  
    454 def parseQueryString(queryString, keep_blank_values=True): 
    455     """Build a paramMap dictionary from a queryString.""" 
    456     if re.match(r"[0-9]+,[0-9]+", queryString): 
    457         # Server-side image map. Map the coords to 'x' and 'y' 
    458         # (like CGI::Request does). 
    459         pm = queryString.split(",") 
    460         pm = {'x': int(pm[0]), 'y': int(pm[1])} 
    461     else: 
    462         pm = cgi.parse_qs(queryString, keep_blank_values) 
    463         for key, val in pm.items(): 
    464             if len(val) == 1: 
    465                 pm[key] = val[0] 
    466     return pm 
    467  
    468 def paramsFromCGIForm(form): 
    469     paramMap = {} 
    470     for key in form.keys(): 
    471         valueList = form[key] 
    472         if isinstance(valueList, list): 
    473             paramMap[key] = [] 
    474             for item in valueList: 
    475                 if item.filename is not None: 
    476                     value = item # It's a file upload 
    477                 else: 
    478                     value = item.value # It's a regular field 
    479                 paramMap[key].append(value) 
    480         else: 
    481             if valueList.filename is not None: 
    482                 value = valueList # It's a file upload 
    483             else: 
    484                 value = valueList.value # It's a regular field 
    485             paramMap[key] = value 
    486     return paramMap 
    487  
    488  
    489 class HeaderMap(dict): 
    490     """A dict subclass for HTTP request and response headers. 
    491      
    492     Each key is changed on entry to str(key).title(). This allows headers 
    493     to be case-insensitive and avoid duplicates. 
    494     """ 
    495      
    496     def __getitem__(self, key): 
    497         return dict.__getitem__(self, str(key).title()) 
    498      
    499     def __setitem__(self, key, value): 
    500         dict.__setitem__(self, str(key).title(), value) 
    501      
    502     def __delitem__(self, key): 
    503         dict.__delitem__(self, str(key).title()) 
    504      
    505     def __contains__(self, item): 
    506         return dict.__contains__(self, str(item).title()) 
    507      
    508     def get(self, key, default=None): 
    509         return dict.get(self, str(key).title(), default) 
    510      
    511     def has_key(self, key): 
    512         return dict.has_key(self, str(key).title()) 
    513      
    514     def update(self, E): 
    515         for k in E.keys(): 
    516             self[str(k).title()] = E[k] 
    517      
    518     def fromkeys(cls, seq, value=None): 
    519         newdict = cls() 
    520         for k in seq: 
    521             newdict[str(k).title()] = value 
    522         return newdict 
    523     fromkeys = classmethod(fromkeys) 
    524      
    525     def setdefault(self, key, x=None): 
    526         key = str(key).title() 
    527         try: 
    528             return self[key] 
    529         except KeyError: 
    530             self[key] = x 
    531             return x 
    532      
    533     def pop(self, key, default): 
    534         return dict.pop(self, str(key).title(), default) 
  • trunk/cherrypy/lib/filter/gzipfilter.py

    r776 r790  
    3434            response.body = self.zip_body(response.body, level) 
    3535         
    36         from cherrypy.lib import cptools 
    37         acceptable = cptools.getAccept('Accept-Encoding') 
     36        from cherrypy.lib import httptools 
     37        h = cherrypy.request.headerMap.get('Accept-Encoding') 
     38        acceptable = httptools.getAccept(h, 'Accept-Encoding') 
    3839        if acceptable is None: 
    3940            # If no Accept-Encoding field is present in a request, 
  • trunk/cherrypy/test/test_core.py

    r788 r790  
    55 
    66import cherrypy 
    7 from cherrypy.lib import cptools 
     7from cherrypy.lib import cptools, httptools 
    88import types 
    99import os 
     
    176176     
    177177    def get_ranges(self): 
    178         return repr(cptools.getRanges(8)) 
     178        h = cherrypy.request.headerMap.get('Range') 
     179        return repr(httptools.getRanges(h, 8)) 
    179180     
    180181    def slice_file(self): 
     
    186187     
    187188    def get_accept(self, headername): 
    188         return "\n".join([str(x) for x in cptools.getAccept(headername)]) 
     189        h = cherrypy.request.headerMap.get(headername) 
     190        return "\n".join([str(x) for x in httptools.getAccept(h, headername)]) 
    189191 
    190192 

Hosted by WebFaction

Log in as guest/cpguest to create tickets