class Message(with_metaclass(HTTPSemantic)): u"""A HTTP message .. seealso:: :rfc:`2616#section-4` """ __slots__ = ('__protocol', '__headers', '__body') @property def protocol(self): return self.__protocol @protocol.setter def protocol(self, protocol): self.__protocol.set(protocol) @property def headers(self): return self.__headers @headers.setter def headers(self, headers): self.__headers.set(headers) @property def body(self): return self.__body @body.setter def body(self, body): self.__body.set(body) @property def trailer(self): return Headers((key, self.headers[key]) for key in self.headers.values('Trailer') if key in self.headers) # @trailer.setter # def trailer(self, trailer): # self.headers.pop('Trailer', None) # if trailer: # trailer = Headers(trailer) # for key in trailer: # self.headers.append('Trailer', key) # self.headers.elements('Trailer') # sanitize # self.headers.merge(trailer) def __init__(self, protocol=None, headers=None, body=None): u"""Initiates a new Message to hold information about the message. :param protocol: the requested protocol :type protocol: str|tuple :param headers: the request headers :type headers: dict or :class:`Headers` :param body: the request body :type body: any """ self.__protocol = Protocol(protocol or (1, 1)) self.__headers = Headers(headers or {}) self.__body = Body(body or b'') def parse(self, protocol): u"""parses the HTTP protocol version :param protocol: the protocol version string :type protocol: bytes """ self.protocol.parse(protocol) def __repr__(self): return '<HTTP Message(protocol=%s)>' % (self.protocol, )
class Protocol(with_metaclass(HTTPSemantic)): u"""The HTTP protocol version""" __slots__ = ('name', '__protocol') @property def version(self): return tuple(self) @property def major(self): return self[0] @property def minor(self): return self[1] PROTOCOL_RE = re.compile(br"^(HTTP)/(\d+).(\d+)\Z") def __init__(self, protocol=(1, 1)): self.__protocol = protocol self.name = b'HTTP' self.set(protocol) def set(self, protocol): if isinstance(protocol, (bytes, Unicode)): protocol = self.parse(protocol) else: self.__protocol = tuple(protocol) def parse(self, protocol): match = self.PROTOCOL_RE.match(protocol) if match is None: raise InvalidLine(_(u"Invalid HTTP protocol: %r"), protocol.decode('ISO8859-1')) self.__protocol = (int(match.group(2)), int(match.group(3))) self.name = match.group(1) def compose(self): return b'%s/%d.%d' % (self.name, self.major, self.minor) def __iter__(self): return self.__protocol.__iter__() def __getitem__(self, key): return self.version[key] def __eq__(self, other): try: other = Protocol(other) except (TypeError, InvalidLine): if isinstance(other, int): return self.major == other return False return self.version == other.version def __lt__(self, other): try: other = Protocol(other) except (TypeError, InvalidLine): if isinstance(other, int): return self.major < other raise # pragma: no cover return self.version < other.version def __gt__(self, other): try: other = Protocol(other) except (TypeError, InvalidLine): if isinstance(other, int): return self.major > other raise # pragma: no cover return self.version > other.version
class URI(with_metaclass(URIType)): u"""Uniform Resource Identifier""" __slots__ = ('scheme', 'username', 'password', 'host', '_port', 'path', 'query_string', 'fragment') SCHEMES = {} SCHEME = None PORT = None encoding = 'UTF-8' @property def query(self): return tuple(QueryString.decode(self.query_string, self.encoding)) @query.setter def query(self, query): self.query_string = QueryString.encode(query, self.encoding) @property def path_segments(self): return [Unicode.replace(p, u'%2f', u'/') for p in self.path.split(u'/')] @path_segments.setter def path_segments(self, path): self.path = u'/'.join(seq.replace(u'/', u'%2f') for seq in path) @property def hostname(self): host = self.host if host.startswith(u'[v') and host.endswith(u']') and u'.' in host and host[2:-1].split(u'.', 1)[0].isdigit(): return host[2:-1].split(u'.', 1)[1] return host.rstrip(u']').lstrip(u'[').lower() @property def port(self): return self._port or self.PORT @port.setter def port(self, port): port = port or self.PORT if port: try: port = int(port) if not 0 < int(port) <= 65535: raise ValueError except ValueError: raise InvalidURI(_(u'Invalid port: %r'), port) # TODO: TypeError self._port = port def __init__(self, uri=None, *args, **kwargs): self.set(kwargs or args or uri or b'') def join(self, other=None, *args, **kwargs): u"""Join a URI with another absolute or relative URI""" relative = URI(other or args or kwargs) joined = URI() current = URI(self) if relative.scheme: current = relative current.normalize() return current joined.scheme = current.scheme if relative.host: current = relative joined.username = current.username joined.password = current.password joined.host = current.host joined.port = current.port if relative.path: current = relative joined.path = current.path if relative.path and not relative.path.startswith(b'/'): joined.path = b'%s%s%s' % (self.path, b'' if self.path.endswith(b'/') else '/../', relative.path) if relative.query_string: current = relative joined.query_string = current.query_string if relative.fragment: current = relative joined.fragment = current.fragment joined.normalize() return joined def normalize(self): u"""Normalize the URI to make it compareable. .. seealso:: :rfc:`3986#section-6` """ self.scheme = self.scheme.lower() self.host = self.host.lower() if not self.port: self.port = self.PORT self.abspath() if not self.path.startswith(u'/') and self.host and self.scheme and self.path: self.path = u'/%s' % (self.path,) def abspath(self): """Clear out any '..' and excessive slashes from the path >>> dangerous = (u'/./', u'/../', u'./', u'/.', u'../', u'/..', u'//') >>> uris = (URI(b'/foo/./bar/../baz//blah/.'), ) >>> _ = [uri.abspath() for uri in uris] >>> all(all(d not in uri.path for d in dangerous) for uri in uris) True >>> u = URI(b'/foo/../bar/.'); u.abspath(); u.path == u'/bar/' True """ path = re.sub(u'\/{2,}', u'/', self.path) # remove // if not path: return unsplit = [] directory = False for part in path.split(u'/'): if part == u'..' and (not unsplit or unsplit.pop() is not None): directory = True elif part != b'.': unsplit.append(part) directory = False else: directory = True if directory: unsplit.append(u'') self.path = u'/'.join(unsplit) or u'/' def set(self, uri): if isinstance(uri, Unicode): uri = uri.encode('UTF-8') # FIXME if isinstance(uri, bytes): self.parse(uri) elif isinstance(uri, URI): self.tuple = uri.tuple elif isinstance(uri, tuple): self.tuple = uri elif isinstance(uri, dict): self.dict = uri else: raise TypeError('URI must be bytes/unicode/tuple/dict not %r' % (type(uri).__name__,)) @property def dict(self): slots = (key.lstrip('_') for key in self.__slots__) return dict((key, getattr(self, key)) for key in slots) @dict.setter def dict(self, uri): for key in self.__slots__: key = key.lstrip('_') setattr(self, key, uri.get(key, u'')) @property def tuple(self): return tuple(getattr(self, key) for key in self.__slots__) @tuple.setter def tuple(self, tuple_): (self.scheme, self.username, self.password, self.host, self.port, self.path, self.query_string, self.fragment) = tuple_ def parse(self, uri): r"""Parses a well formed absolute or relative URI. foo://example.com:8042/over/there?name=ferret#nose \_/ \______________/\_________/ \_________/ \__/ | | | | | scheme authority path query fragment | _____________________|__ / \ / \ urn:example:animal:ferret:nose https://username:password@[::1]:8090/some/path?query#fragment <scheme>://<username>:<password>@<host>:<port>/<path>?<query>#<fragment> [<scheme>:][//[<username>[:<password>]@][<host>][:<port>]/]<path>[?<query>][#<fragment>] """ if isinstance(uri, Unicode): try: uri = uri.encode('ascii') except UnicodeEncodeError: raise TypeError('URI must be ASCII bytes.') if type(self) is URI and b':' in uri: self.scheme = uri.split(b':', 1)[0].lower() if type(self) is not URI: return self.parse(uri) if uri and uri.strip(b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'): raise InvalidURI(_(u'Invalid URI: must consist of printable ASCII characters without whitespace.')) uri, __, fragment = uri.partition(b'#') uri, __, query_string = uri.partition(b'?') scheme, authority_exists, uri = uri.rpartition(b'://') if not authority_exists and uri.startswith(b'//'): uri = uri[2:] authority_exists = True if not authority_exists and b':' in uri: scheme, __, uri = uri.partition(b':') authority, path = b'', uri if authority_exists: authority, __, path = uri.partition(b'/') path = b'%s%s' % (__, path) userinfo, __, hostport = authority.rpartition(b'@') username, __, password = userinfo.partition(b':') if b':' in hostport and not hostport.endswith(b']'): host, __, port = hostport.rpartition(b':') else: host, port = hostport, b'' unquote = self.unquote path = u'/'.join([unquote(seq).replace(u'/', u'%2f') for seq in path.split(b'/')]) try: scheme = scheme.decode('ascii').lower() except UnicodeDecodeError: raise InvalidURI(_(u'Invalid scheme: must be ASCII.')) if scheme and scheme.strip(u'abcdefghijklmnopqrstuvwxyz0123456789.-+'): raise InvalidURI(_(u'Invalid scheme: must only contain alphanumeric letters or plus, dash, dot.')) if query_string: query_string = QueryString.encode(QueryString.decode(query_string, self.encoding), self.encoding) self.tuple = ( scheme, unquote(username), unquote(password), self._unquote_host(host), port, path, query_string.decode(self.encoding), unquote(fragment) ) def _unquote_host(self, host): # IPv6 / IPvFuture if host.startswith(b'[') and host.endswith(b']'): host = host[1:-1] try: return u'[%s]' % inet_ntop(AF_INET6, inet_pton(AF_INET6, host)).decode('ascii') except SocketError: # IPvFuture if host.startswith(b'v') and b'.' in host and host[1:].split(b'.', 1)[0].isdigit(): try: return u'[%s]' % host.decode('ascii') except UnicodeDecodeError: raise InvalidURI(_('Invalid IPvFuture address: must be ASCII.')) raise InvalidURI(_('Invalid IP address in URI.')) # IPv4 if all(x.isdigit() for x in host.split(b'.')): try: return inet_ntop(AF_INET, inet_pton(AF_INET, host)).decode('ascii') except SocketError: raise InvalidURI(_('Invalid IPv4 address in URI.')) if host.strip(Percent.UNRESERVED + Percent.SUB_DELIMS + b'%'): raise InvalidURI(_('Invalid URI host.')) # DNS hostname host = self.unquote(host) try: return host.encode('ascii').decode('idna').lower() except UnicodeError: raise InvalidURI(_('Invalid host.')) def compose(self): return b''.join(self._compose_absolute_iter()) def _compose_absolute_iter(self): u"""composes the whole URI""" scheme, username, password, host, port, path, _, fragment = self.tuple if scheme: yield self.quote(scheme, Percent.SCHEME) yield b':' authority = b''.join(self._compose_authority_iter()) if authority: yield b'//' yield authority yield b''.join(self._compose_relative_iter()) def _compose_authority_iter(self): if not self.host: return username, password, host, port, quote = self.username, self.password, self.host, self.port, self.quote if username: yield quote(username, Percent.USERINFO) if password: yield b':' yield quote(password, Percent.USERINFO) yield b'@' try: yield host.encode('idna') except UnicodeError: # u'..'.encode('idna') raise InvalidURI(_(u'Invalid URI: cannot encode host as IDNA.')) if port and int(port) != self.PORT: yield b':%d' % int(port) def _compose_relative_iter(self): u"""Composes the relative URI beginning with the path""" scheme, path, query_string, quote, fragment = self.scheme, self.path, self.query_string, self.quote, self.fragment PATH = Percent.PATH if not scheme and not path.startswith(u'/'): PATH = set(PATH) - {b':', b'@'} yield b'/'.join(quote(x, PATH) for x in path.split(u'/')) if query_string: yield b'?' yield query_string if fragment: yield b'#' yield quote(fragment, Percent.FRAGMENT) def unquote(self, data): return Percent.unquote(bytes(data)).decode(self.encoding) def quote(self, data, charset): return Percent.quote(Unicode(data).encode(self.encoding), charset) def __eq__(self, other): u"""Compares the URI with another string or URI .. seealso:: :rfc:`2616#section-3.2.3` .. seealso:: :rfc:`3986#section-6` >>> u1 = URI(b'http://abc.com:80/~smith/home.html') >>> u2 = b'http://ABC.com/%7Esmith/home.html' >>> u3 = URI(b'http://ABC.com:/%7esmith/home.html') >>> u1 == u2 == u3 True """ cls = type(self) self_, other = cls(self), cls(other) self_.normalize() other.normalize() return self_.tuple == other.tuple def __setattr__(self, name, value): if name.startswith('_'): return super(URI, self).__setattr__(name, value) if name == 'scheme' and value: self.__class__ = self.SCHEMES.get(value, URI) if name in self.__slots__: if isinstance(value, bytes): try: value = value.decode('UTF-8') except UnicodeDecodeError: value = value.decode('ISO8859-1') if value is None: pass elif not isinstance(value, Unicode): raise TypeError('%r must be string, not %s' % (name, type(value).__name__)) super(URI, self).__setattr__(name, value) def __repr__(self): return '<URI(%s)>' % bytes(self)
class HeaderElement(with_metaclass(HeaderType)): u"""An element (with parameters) from an HTTP header's element list.""" priority = None hop_by_hop = False list_element = False # Regular expression that matches `special' characters in parameters, the # existance of which force quoting of the parameter value. RE_TSPECIALS = re.compile(b'[ \(\)<>@,;:\\\\"/\[\]\?=]') RE_SPLIT = re.compile(b',(?=(?:[^"]*"[^"]*")*[^"]*$)') RE_PARAMS = re.compile(b';(?=(?:[^"]*"[^"]*")*[^"]*$)') def __init__(self, value, params=None): self.value = bytes(value) self.params = params or {} self.sanitize() def sanitize(self): pass def __lt__(self, other): return self.value < getattr(other, 'value', other) def __gt__(self, other): return self.value > getattr(other, 'value', other) def __eq__(self, other): return self.value == getattr(other, 'value', other) def __ne__(self, other): return not self == other def __bytes__(self): return self.compose() def __unicode__(self): return self.decode(bytes(self)) if str is bytes: __str__ = __bytes__ else: # pragma: no cover __str__ = __unicode__ def compose(self): params = [ b'; %s' % self.formatparam(k, v) for k, v in iteritems(self.params) ] return b'%s%s' % (self.value, ''.join(params)) @classmethod def parseparams(cls, elementstr): """Transform 'token;key=val' to ('token', {'key': 'val'}).""" # Split the element into a value and parameters. The 'value' may # be of the form, "token=token", but we don't split that here. assert isinstance(elementstr, bytes) atoms = [ x.strip() for x in cls.RE_PARAMS.split(elementstr) if x.strip() ] or [b''] value = atoms.pop(0) params = (cls.parseparam(atom) for atom in atoms) params = cls._rfc2231_and_continuation_params(params) # TODO: prefer foo* parameter when both are provided return value, dict(params) @classmethod def parseparam(cls, atom): key, __, val = atom.partition(b'=') try: val, quoted = cls.unescape_param(val.strip()) except InvalidHeader: raise InvalidHeader( _(u'Unquoted parameter %r in %r containing TSPECIALS: %r'), key, cls.__name__, val) return cls.unescape_key(key), val, quoted @classmethod def unescape_key(cls, key): return key.strip().lower() @classmethod def unescape_param(cls, value): quoted = value.startswith(b'"') and value.endswith(b'"') if quoted: value = re.sub(r'\\(?!\\)', '', value.strip(b'"')) else: if cls.RE_TSPECIALS.search(value): raise InvalidHeader( _(u'Unquoted parameter in %r containing TSPECIALS: %r'), cls.__name__, value) return value, quoted @classmethod def _rfc2231_and_continuation_params(cls, params): # TODO: complexity count = set() continuations = dict() for key, value, quoted in params: if key in count: raise InvalidHeader(_(u'Parameter given twice: %r'), key.decode('ISO8859-1')) count.add(key) if '*' in key: if key.endswith('*') and not quoted: charset, language, value_ = decode_rfc2231( value.encode('ISO8859-1')) if not charset: yield key, value continue encoding = sanitize_encoding(charset) if encoding is None: raise InvalidHeader(_(u'Unknown encoding: %r'), charset) try: key, value = key[:-1], Percent.unquote(value_).decode( encoding) except UnicodeDecodeError as exc: raise InvalidHeader(_(u'%s') % (exc, )) key_, asterisk, num = key.rpartition('*') if asterisk: try: if num != '0' and num.startswith('0'): raise ValueError num = int(num) except ValueError: yield key, value continue continuations.setdefault(key_, {})[num] = value continue yield key, value for key, lines in iteritems(continuations): value = b'' for i in xrange(len(lines)): try: value += lines.pop(i) except KeyError: break if not key: raise InvalidHeader(_(u'...')) if value: yield key, value for k, v in iteritems(lines): yield '%s*%d' % (key, k), v @classmethod def parse(cls, elementstr): """Construct an instance from a string of the form 'token;key=val'.""" ival, params = cls.parseparams(elementstr) return cls(ival, params) @classmethod def split(cls, fieldvalue): return cls.RE_SPLIT.split(fieldvalue) @classmethod def join(cls, values): return b', '.join(values) @classmethod def sorted(cls, elements): return elements @classmethod def merge(cls, elements, others): return cls.join([bytes(x) for x in cls.sorted(elements + others)]) @classmethod def formatparam(cls, param, value=None, quote=False): """Convenience function to format and return a key=value pair. This will quote the value if needed or if quote is true. """ if value: value = bytes(value) if quote or cls.RE_TSPECIALS.search(value): value = value.replace(b'\\', b'\\\\').replace(b'"', br'\"') return b'%s="%s"' % (param, value) else: return b'%s=%s' % (param, value) else: return param @classmethod def decode(cls, value): if b'=?' in value: # FIXME: must not parse encoded_words in unquoted ('Content-Type', 'Content-Disposition') header params return u''.join( atom.decode(charset or 'ISO8859-1') for atom, charset in decode_header(value)) return value.decode('ISO8859-1') @classmethod def encode(cls, value): try: return value.encode('ISO8859-1') except UnicodeEncodeError: return value.encode( 'ISO8859-1', 'replace' ) # FIXME: if value contains UTF-8 chars encode them in MIME; =?UTF-8?B?…?= (RFC 2047); seealso quopri def __repr__(self): params = ', %r' % (self.params, ) if self.params else '' return '<%s(%r%s)>' % (self.__class__.__name__, self.value, params)
class Headers(with_metaclass(HTTPSemantic, CaseInsensitiveDict)): # disallowed bytes for HTTP header field names HEADER_RE = re.compile(br"[\x00-\x1F\x7F()<>@,;:\\\\\"/\[\]?={} \t\x80-\xFF]") @staticmethod def formatvalue(value): return to_unicode(value) # TODO: using unicode here is not good if processed via HeaderElement @classmethod def formatkey(cls, key): key = CaseInsensitiveDict.formatkey(key) if cls.HEADER_RE.search(key): raise InvalidHeader(_(u"Invalid header name: %r"), key.decode('ISO8859-1')) return key # TODO: do we want bytes here? def elements(self, fieldname): u"""Return a sorted list of HeaderElements from the given comma-separated header string.""" fieldvalue = self.get(fieldname) if not fieldvalue: return [] Element = HEADER.get(fieldname, HeaderElement) return Element.sorted([Element.parse(element) for element in Element.split(fieldvalue.encode('ascii'))]) def element(self, fieldname, default=None): u"""Treat the field as single element""" if fieldname in self: Element = HEADER.get(fieldname, HeaderElement) return Element.parse(self[fieldname]) return default # # TODO: a really nice alternative method would be: # def element(self, fieldname, which=None, default=None): # for element in self.elements(fieldname): # if which is None or element == which: # return element # return default def set_element(self, fieldname, *args, **kwargs): self[fieldname] = bytes(self.create_element(fieldname, *args, **kwargs)) def append_element(self, fieldname, *args, **kwargs): self.append(fieldname, bytes(self.create_element(fieldname, *args, **kwargs))) def create_element(self, fieldname, *args, **kwargs): Element = HEADER.get(fieldname, HeaderElement) return Element(*args, **kwargs) def values(self, key=None): # FIXME: overwrites dict.values() if key is None: return super(Headers, self).values() # if key is set return a ordered list of element values return [e.value for e in self.elements(key)] def append(self, _name, _value, **params): if params: Element = HEADER.get(_name, HeaderElement) parts = [_value or b''] for k, v in iteritems(params): k = k.replace('_', '-') # TODO: find out why this is done if v is None: parts.append(k) else: parts.append(Element.formatparam(k, v)) _value = "; ".join(parts) if _name not in self or not self[_name]: self[_name] = _value else: Element = HEADER.get(_name, HeaderElement) self[_name] = Element.join([self[_name], _value]) def validate(self): u"""validates all header elements :raises: InvalidHeader """ for name in self: self.elements(name) def merge(self, other): other = self.__class__(other) for key in other: Element = HEADER.get(key, HeaderElement) self[key] = Element.merge(self.elements(key), other.elements(key)) def set(self, headers): self.clear() self.update(headers) def parse(self, data): r"""parses HTTP headers :param data: the header string containing headers separated by "\r\n" without trailing "\r\n" :type data: bytes """ lines = data.split(b'\r\n') while lines: curr = lines.pop(0) name, __, value = curr.partition(b':') if __ != b':': raise InvalidHeader(_(u"Invalid header line: %r"), curr.decode('ISO8859-1')) if self.HEADER_RE.search(name): raise InvalidHeader(_(u"Invalid header name: %r"), name.decode('ISO8859-1')) name, value = name.strip(), [value.lstrip()] # continuation lines while lines and lines[0].startswith((b' ', b'\t')): value.append(lines.pop(0)[1:]) value = b''.join(value).rstrip() Element = HEADER.get(name, HeaderElement) value = Element.decode(value) self.append(name, value) def compose(self): return b'%s\r\n' % b''.join(b'%s: %s\r\n' % (k, v) for k, v in self.__items()) def __items(self): return sorted(self.__encoded_items(), key=lambda x: HEADER.get(x[0], HeaderElement).priority or x[0]) def __encoded_items(self): for key, values in iteritems(self): Element = HEADER.get(key, HeaderElement) if Element is not HeaderElement: key = Element.__name__ if Element.list_element: for value in Element.split(values): yield key, Element.encode(value) else: yield key, Element.encode(values) def __repr__(self): return "<HTTP Headers(%s)>" % repr(list(self.items()))
class StatusException(with_metaclass(StatusType, Status, Exception)): u"""This class represents a small HTTP Response message for error handling purposes""" @property def headers(self): return self._headers @property def body(self): if not hasattr(self, '_body'): from httoop.messages.body import Body self._body = Body(mimetype='application/json') self._body.data = self.to_dict() return self._body @body.setter def body(self, value): self.body self._body.set(value) header_to_remove = () u"""a tuple of header field names which should be removed when responding with this error""" description = '' @property def traceback(self): return self._traceback @traceback.setter def traceback(self, tb): if self.server_error: self._traceback = tb code = 0 def __init__(self, description=None, reason=None, headers=None, traceback=None): u""" :param description: a description of the error which happened :type description: str :param reason: a additional reason phrase :type reason: str :param headers: :type headers: dict :param traceback: A Traceback for the error :type traceback: str """ Status.__init__(self, self.__class__.code, reason=reason) self._headers = dict() self._traceback = None if isinstance(headers, dict): self._headers.update(headers) if description is not None: self.description = description if traceback: self.traceback = traceback def __repr__(self): return '<HTTP Status %d %r>' % (int(self), self.reason) __str__ = __repr__ def to_dict(self): u"""the default body arguments""" return dict(status=self.status, reason=self.reason, description=self.description, headers=self.headers)
class Body(with_metaclass(HTTPSemantic, IFile)): u"""A HTTP message body This class is capable of handling HTTP Transfer-Encoding and Content-Encoding as defined in RFC 2616. It provides an interface which makes it possible to use either unicode, bytes, bytearray, file, file-like objects (e.g. from the codecs module), StringIO, BytesIO, NamedTemporaryFiles or any iterable returning bytes/unicode as type for the content. The encode and decode methods can also control the automatic en/decoding of the content using the codec specified in the MIME media type. """ __slots__ = ('fd', 'data', '__iter', 'headers', 'trailer', 'content_codec', 'transfer_codec') MAX_CHUNK_SIZE = 4096 @property def fileable(self): u"""Flag whether the set content provides the file interface""" return all( hasattr(self.fd, method) for method in ('read', 'write', 'close')) @property def generator(self): return isinstance(self.fd, GeneratorType) @property def encoding(self): return self.mimetype.charset or 'UTF-8' @encoding.setter def encoding(self, charset): mimetype = self.mimetype mimetype.charset = charset self.mimetype = mimetype @property def mimetype(self): u"""Represents the MIME media type of the content""" return self.headers.element('Content-Type') @mimetype.setter def mimetype(self, mimetype): self.headers['Content-Type'] = bytes(mimetype) @property def content_encoding(self): return self.headers.element('Content-Encoding') @content_encoding.setter def content_encoding(self, value): if value: self.headers['Content-Encoding'] = bytes(value) self.content_codec = None #self.content_encoding.iterdecode() else: self.headers.pop('Content-Encoding', None) self.content_codec = None @property def transfer_encoding(self): return self.headers.element('Transfer-Encoding') @transfer_encoding.setter def transfer_encoding(self, transfer_encoding): if transfer_encoding: self.headers['Transfer-Encoding'] = bytes(transfer_encoding) self.transfer_codec = None #self.transfer_encoding.iterdecode() else: self.headers.pop('Transfer-Encoding', None) self.transfer_codec = None @property def chunked(self): if not self.transfer_encoding: return False return 'chunked' == self.transfer_encoding.value @chunked.setter def chunked(self, chunked): self.transfer_encoding = 'chunked' if chunked else None def __init__(self, content=None, mimetype=None): self.data = None self.__iter = None self.fd = BytesIO() self.headers = Headers() self.trailer = Headers() self.transfer_codec = None self.content_codec = None self.mimetype = mimetype or b'text/plain; charset=UTF-8' self.set(content) def encode(self, *data): u"""Encode the object in :attr:`data` if a codec for the mimetype exists""" codec = self.mimetype.codec if codec: if self.data is None and not data: return data = data[0] if data else self.data value = codec.encode(data, self.encoding, self.mimetype) self.set(value) self.data = data def iterencode(self, *data): codec = self.mimetype.codec if codec: data = data[0] if data else self.data value = codec.iterencode(data, self.encoding, self.mimetype) self.set(value) self.data = data def decode(self, *data): u"""Decodes the body content if a codec for the mimetype exists. Stores the decoded object in :attr:`data` """ codec = self.mimetype.codec if data: self.set(data[0]) if codec: self.data = codec.decode(self.__content_bytes(), self.encoding, self.mimetype) return self.data def compress(self): u"""Applies the Content-Encoding codec to the content""" codec = self.content_codec if codec: self.set(codec.encode(self.__content_bytes())) def set(self, content): if isinstance(content, Body): self.mimetype = content.mimetype self.data = content.data self.chunked = content.chunked self.trailer = content.trailer self.fd = content.fd return self.data = None if not content: content = BytesIO() elif isinstance(content, BytesIO) or (hasattr(content, 'read') and hasattr(content, 'fileno') and hasattr(content, 'closed')): if content.closed: raise ValueError('I/O operation on closed file.') elif isinstance(content, Unicode): content = BytesIO(content.encode(self.encoding)) elif isinstance(content, bytes): content = BytesIO(content) elif isinstance(content, bytearray): content = BytesIO(bytes(content)) elif not hasattr(content, '__iter__'): raise TypeError('Content must be iterable.') self.fd = content def parse(self, data): if self.transfer_codec: data = self.transfer_codec.decode(data) if self.content_codec: data = self.content_codec.decode(data) self.write(data) def compose(self): return b''.join(self.__iter__()) def close(self): fileable = self.fileable super(Body, self).close() if fileable: self.set('') def __unicode__(self): return self.__content_bytes().decode(self.encoding) def __iter__(self): u"""Iterates over the content applying Content-Encoding and Transfer-Encoding""" data = self.__content_iter() for codec in (self.content_codec, self.transfer_codec): if codec: data = (codec.encode(d) for d in data) if self.chunked: data = self.__compose_chunked_iter(data) return data def __compose_chunked_iter(self, iterable): for data in iterable: if not data: continue yield b"%x\r\n%s\r\n" % (len(data), data) if self.trailer: yield b"0\r\n%s" % bytes(self.trailer) else: yield b"0\r\n\r\n" def __content_bytes(self): return b''.join(self.__content_iter()) def __content_iter(self): iterable = self.__iterable() t = self.tell() self.seek(0) try: for data in iterable: if data is None: continue if isinstance(data, Unicode): data = data.encode(self.encoding) elif not isinstance(data, bytes): # pragma: no cover raise TypeError('Iterable contained non-bytes: %r' % (type(data).__name__, )) yield data finally: self.seek(t) def __iterable(self): if self.fileable: return self.__iter_fileable() elif self.generator: return self.__iter_generator() return self.fd def __iter_fileable(self, chunksize=MAX_CHUNK_SIZE): data = self.read(chunksize) while data: yield data data = self.read(chunksize) def __iter_generator(self): fd = self.fd buffer_ = [] while True: try: data = next(fd) except StopIteration: self.set(buffer_) raise else: buffer_.append(data) yield data # def __copy__(self): # body = self.__class__(self.__content_bytes()) # body.mimetype = self.mimetype # body.data = self.data # body.transfer_encoding = self.transfer_encoding # body.content_encoding = self.content_encoding # return body def __bool__(self): return bool(len(self)) def __len__(self): body = self.fd if isinstance(body, BytesIO): return len(body.getvalue()) if hasattr(body, 'fileno'): return fstat(body.fileno()).st_size return len(self.__content_bytes()) def __next__(self): if self.__iter is None: self.__iter = self.__iter__() try: return next(self.__iter) except StopIteration: self.__iter = None raise next = __next__
class Date(with_metaclass(HTTPSemantic)): u"""A HTTP Date string It provides a API to multiple time representations: * datetime * time struct * UNIX timestamp Supported HTTP date string formats: :example: Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format """ def __init__(self, timeval=None): u""" :param timeval: :type timeval: either seconds since epoch in float or a datetime object or a timetuple """ self.__composed = None self.__timestamp = None self.__datetime = None self.__time_struct = None if timeval is None: self.__timestamp = time.time() elif isinstance(timeval, (float, int)): self.__timestamp = float(timeval) elif isinstance(timeval, (tuple, time.struct_time)): # self.__timestamp = calendar.timegm(timeval) self.__timestamp = time.mktime(timeval) - time.timezone elif isinstance(timeval, datetime): self.__datetime = timeval # self.__timestamp = calendar.timegm(self.datetime.utctimetuple()) self.__timestamp = time.mktime( self.datetime.utctimetuple()) - time.timezone elif isinstance(timeval, (bytes, Unicode)): if isinstance(timeval, Unicode): timeval = timeval.encode('ascii', 'ignore') self.__timestamp = float(Date.parse(timeval)) else: raise TypeError('Date(): got invalid argument') @property def datetime(self): if self.__datetime is None: self.__datetime = datetime.utcfromtimestamp(int(self)) return self.__datetime @property def gmtime(self): if self.__time_struct is None: self.__time_struct = time.gmtime(int(self)) return self.__time_struct def compose(self): if self.__composed is None: self.__composed = self.__compose() return self.__composed def __compose(self): d = self.gmtime return b'%s, %02d %s %04d %02d:%02d:%02d GMT' % ( (b'Mon', b'Tue', b'Wed', b'Thu', b'Fri', b'Sat', b'Sun')[d.tm_wday], d.tm_mday, (b'Jan', b'Feb', b'Mar', b'Apr', b'May', b'Jun', b'Jul', b'Aug', b'Sep', b'Oct', b'Nov', b'Dec')[d.tm_mon - 1], d.tm_year, d.tm_hour, d.tm_min, d.tm_sec) @classmethod def parse(cls, timestr=None): u"""parses a HTTP date string and returns a :class:`Date` object :param timestr: the time string in one of the http formats :type timestr: str :returns: the HTTP Date object :rtype : :class:`Date` """ # parse the most common HTTP Date format (RFC 2822) date = parsedate(timestr) if date is not None: return cls(date[:9]) old = locale.getlocale(locale.LC_TIME) locale.setlocale(locale.LC_TIME, (None, None)) try: # parse RFC 1036 date format try: date = time.strptime(timestr, '%A, %d-%b-%y %H:%M:%S GMT') except ValueError: pass else: return cls(date) # parse C's asctime format try: date = time.strptime(timestr, '%a %b %d %H:%M:%S %Y') except ValueError: pass else: return cls(date) finally: locale.setlocale(locale.LC_TIME, old) raise InvalidDate(_(u'Invalid date: %r'), date) def __int__(self): return int(float(self)) def __float__(self): return float(self.__timestamp) def __eq__(self, other): try: return int(self) == int(self.__other(other)) except NotImplementedError: return NotImplemented def __gt__(self, other): try: return int(self) > int(self.__other(other)) except NotImplementedError: return NotImplemented def __lt__(self, other): try: return int(self) < int(self.__other(other)) except NotImplementedError: return NotImplemented def __other(self, other): if other is None: raise NotImplementedError if isinstance(other, Date): return other try: return Date(other) except (InvalidDate, TypeError): raise NotImplementedError def __repr__(self): return '<HTTP Date(%d)>' % (int(self), )
class Status(with_metaclass(HTTPSemantic)): u"""A HTTP Status :rfc:`2616#section-6.2` """ # __slots__ = ('__code', '__reason') @property def informational(self): return 99 < self.__code < 200 @property def successful(self): return 199 < self.__code < 300 @property def redirection(self): return 299 < self.__code < 400 @property def client_error(self): return 399 < self.__code < 500 @property def server_error(self): return 499 < self.__code < 600 # aliases @property def status(self): return self.__code @property def reason_phrase(self): return self.__reason reason = None @property def code(self): return self.__code @code.setter def code(self, code): self.set((code, self.__reason)) @property def reason(self): return self.__reason @reason.setter def reason(self, reason): self.set((self.__code, reason)) STATUS_RE = re.compile(br"^([1-5]\d{2})(?:\s+([\s\w]*))\Z") def __init__(self, code=None, reason=None): """ :param code: the HTTP Statuscode :type code: int :param reason: the HTTP Reason-Phrase :type reason: unicode """ self.__code = 0 reason = reason or u'' reason = reason or reason or REASONS.get(code, ('', ''))[0] if code: self.set(( code, reason, )) def parse(self, status): """parse a Statuscode and Reason-Phrase :param status: the code and reason :type status: bytes """ match = self.STATUS_RE.match(status) if match is None: raise InvalidLine(_(u"Invalid status %r"), status.decode('ISO8859-1')) self.set(( int(match.group(1)), match.group(2).decode('ascii'), )) def compose(self): return b'%d %s' % (self.__code, self.__reason.encode('ascii')) def __unicode__(self): return self.compose().decode('ascii') def __int__(self): u"""Returns this status as number""" return self.__code def __eq__(self, other): u"""Compares a status with another :class:`Status` or :class:`int`""" if isinstance(other, int): return self.__code == other if isinstance(other, Status): return self.__code == other.code return super(Status, self).__eq__(other) def __lt__(self, other): return self.__code < other def __gt__(self, other): return self.__code > other def set(self, status): u"""sets reason and status :param status: A HTTP Status, e.g.: 200, (200, 'OK'), '200 OK' :type status: int or tuple or bytes or Status """ if isinstance(status, int) and 99 < status < 600: self.__code, self.__reason = status, REASONS.get( status, (u'', u''))[0] elif isinstance(status, tuple): code, reason = status if isinstance(reason, bytes): reason = reason.decode('ascii') self.__code, self.__reason = int(code), reason elif isinstance(status, (bytes, Unicode)): code, reason = status.split(None, 1) if isinstance(reason, bytes): reason = reason.decode('ascii') self.__code, self.__reason = int(code), reason elif isinstance(status, Status): self.__code, self.__reason = status.code, status.reason else: raise TypeError('invalid status') def __repr__(self): return '<HTTP Status (code=%d, reason=%r)>' % (self.__code, self.__reason)