0001"""
0002Does parsing of ETag-related headers: If-None-Matches, If-Matches
0003
0004Also If-Range parsing
0005"""
0006
0007import webob
0008
0009__all__ = ['AnyETag', 'NoETag', 'ETagMatcher', 'IfRange', 'NoIfRange']
0010
0011class _AnyETag(object):
0012    """
0013    Represents an ETag of *, or a missing ETag when matching is 'safe'
0014    """
0015
0016    def __repr__(self):
0017        return '<ETag *>'
0018
0019    def __nonzero__(self):
0020        return False
0021
0022    def __contains__(self, other):
0023        return True
0024
0025    def weak_match(self, other):
0026        return True
0027
0028    def __str__(self):
0029        return '*'
0030
0031AnyETag = _AnyETag()
0032
0033class _NoETag(object):
0034    """
0035    Represents a missing ETag when matching is unsafe
0036    """
0037
0038    def __repr__(self):
0039        return '<No ETag>'
0040
0041    def __nonzero__(self):
0042        return False
0043
0044    def __contains__(self, other):
0045        return False
0046
0047    def weak_match(self, other):
0048        return False
0049
0050    def __str__(self):
0051        return ''
0052
0053NoETag = _NoETag()
0054
0055class ETagMatcher(object):
0056
0057    """
0058    Represents an ETag request.  Supports containment to see if an
0059    ETag matches.  You can also use
0060    ``etag_matcher.weak_contains(etag)`` to allow weak ETags to match
0061    (allowable for conditional GET requests, but not ranges or other
0062    methods).
0063    """
0064
0065    def __init__(self, etags, weak_etags=()):
0066        self.etags = etags
0067        self.weak_etags = weak_etags
0068
0069    def __contains__(self, other):
0070        return other in self.etags
0071
0072    def weak_match(self, other):
0073        if other.lower().startswith('w/'):
0074            other = other[2:]
0075        return other in self.etags or other in self.weak_etags
0076
0077    def __repr__(self):
0078        return '<ETag %s>' % (
0079            ' or '.join(self.etags))
0080
0081    def parse(cls, value):
0082        """
0083        Parse this from a header value
0084        """
0085        results = []
0086        weak_results = []
0087        while value:
0088            if value.lower().startswith('w/'):
0089                # Next item is weak
0090                weak = True
0091                value = value[2:]
0092            else:
0093                weak = False
0094            if value.startswith('"'):
0095                try:
0096                    etag, rest = value[1:].split('"', 1)
0097                except ValueError:
0098                    etag = value.strip(' ",')
0099                    rest = ''
0100                else:
0101                    rest = rest.strip(', ')
0102            else:
0103                if ',' in value:
0104                    etag, rest = value.split(',', 1)
0105                    rest = rest.strip()
0106                else:
0107                    etag = value
0108                    rest = ''
0109            if etag == '*':
0110                return AnyETag
0111            if etag:
0112                if weak:
0113                    weak_results.append(etag)
0114                else:
0115                    results.append(etag)
0116            value = rest
0117        return cls(results, weak_results)
0118    parse = classmethod(parse)
0119
0120    def __str__(self):
0121        # FIXME: should I quote these?
0122        items = list(self.etags)
0123        for weak in self.weak_etags:
0124            items.append('W/%s' % weak)
0125        return ', '.join(items)
0126
0127class IfRange(object):
0128    """
0129    Parses and represents the If-Range header, which can be
0130    an ETag *or* a date
0131    """
0132    def __init__(self, etag=None, date=None):
0133        self.etag = etag
0134        self.date = date
0135
0136    def __repr__(self):
0137        if self.etag is None:
0138            etag = '*'
0139        else:
0140            etag = str(self.etag)
0141        if self.date is None:
0142            date = '*'
0143        else:
0144            date = webob._serialize_date(self.date)
0145        return '<%s etag=%s, date=%s>' % (
0146            self.__class__.__name__,
0147            etag, date)
0148
0149    def __str__(self):
0150        if self.etag is not None:
0151            return str(self.etag)
0152        elif self.date:
0153            return webob._serialize_date(self.date)
0154        else:
0155            return ''
0156
0157    def match(self, etag=None, last_modified=None):
0158        """
0159        Return True if the If-Range header matches the given etag or last_modified
0160        """
0161        if self.date is not None:
0162            if last_modified is None:
0163                # Conditional with nothing to base the condition won't work
0164                return False
0165            return last_modified <= self.date
0166        elif self.etag is not None:
0167            if not etag:
0168                return False
0169            return etag in self.etag
0170        return True
0171
0172    def match_response(self, response):
0173        """
0174        Return True if this matches the given ``webob.Response`` instance.
0175        """
0176        return self.match(etag=response.etag, last_modified=response.last_modified)
0177
0178    #@classmethod
0179    def parse(cls, value):
0180        """
0181        Parse this from a header value.
0182        """
0183        date = etag = None
0184        if not value:
0185            etag = NoETag()
0186        elif value and value.endswith(' GMT'):
0187            # Must be a date
0188            date = webob._parse_date(value)
0189        else:
0190            etag = ETagMatcher.parse(value)
0191        return cls(etag=etag, date=date)
0192    parse = classmethod(parse)
0193
0194class _NoIfRange(object):
0195    """
0196    Represents a missing If-Range header
0197    """
0198
0199    def __repr__(self):
0200        return '<Empty If-Range>'
0201
0202    def __str__(self):
0203        return ''
0204
0205    def __nonzero__(self):
0206        return False
0207
0208    def match(self, etag=None, last_modified=None):
0209        return True
0210
0211    def match_response(self, response):
0212        return True
0213
0214NoIfRange = _NoIfRange()