Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(212)

Unified Diff: tools/telemetry/third_party/gsutil/third_party/httplib2/python3/httplib2/__init__.py

Issue 1258583006: Add gsutil 4.13 to telemetry/third_party (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Undo all other changes so this just add gsutil to third_party Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tools/telemetry/third_party/gsutil/third_party/httplib2/python3/httplib2/__init__.py
diff --git a/chrome/common/extensions/docs/examples/apps/hello-python/httplib2/__init__.py b/tools/telemetry/third_party/gsutil/third_party/httplib2/python3/httplib2/__init__.py
similarity index 65%
copy from chrome/common/extensions/docs/examples/apps/hello-python/httplib2/__init__.py
copy to tools/telemetry/third_party/gsutil/third_party/httplib2/python3/httplib2/__init__.py
index 3cebcb32131153192d608fd86b4068c131958f61..bf6c2e9b4b9cf96327e42a70a3ba7ffdf73a62e3 100644
--- a/chrome/common/extensions/docs/examples/apps/hello-python/httplib2/__init__.py
+++ b/tools/telemetry/third_party/gsutil/third_party/httplib2/python3/httplib2/__init__.py
@@ -1,13 +1,14 @@
-from __future__ import generators
+
"""
httplib2
A caching http interface that supports ETags and gzip
-to conserve bandwidth.
+to conserve bandwidth.
-Requires Python 2.3 or later
+Requires Python 3.0 or later
Changelog:
+2009-05-28, Pilgrim: ported to Python 3
2007-08-18, Rick: Modified so it's able to use a socks proxy if needed.
"""
@@ -20,96 +21,66 @@ __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)",
"Jonathan Feinberg",
"Blair Zajac",
"Sam Ruby",
- "Louis Nyffenegger"]
+ "Louis Nyffenegger",
+ "Mark Pilgrim"]
__license__ = "MIT"
-__version__ = "$Rev$"
+__version__ = "0.7.7"
-import re
-import sys
+import re
+import sys
import email
-import email.Utils
-import email.Message
-import email.FeedParser
-import StringIO
+import email.utils
+import email.message
+import email.feedparser
+import io
import gzip
import zlib
-import httplib
-import urlparse
+import http.client
+import urllib.parse
import base64
import os
import copy
import calendar
import time
import random
-# remove depracated warning in python2.6
-try:
- from hashlib import sha1 as _sha, md5 as _md5
-except ImportError:
- import sha
- import md5
- _sha = sha.new
- _md5 = md5.new
+import errno
+from hashlib import sha1 as _sha, md5 as _md5
import hmac
from gettext import gettext as _
import socket
+import ssl
+_ssl_wrap_socket = ssl.wrap_socket
try:
import socks
except ImportError:
socks = None
-# Build the appropriate socket wrapper for ssl
-try:
- import ssl # python 2.6
- _ssl_wrap_socket = ssl.wrap_socket
-except ImportError:
- def _ssl_wrap_socket(sock, key_file, cert_file):
- ssl_sock = socket.ssl(sock, key_file, cert_file)
- return httplib.FakeSocket(sock, ssl_sock)
-
-
-if sys.version_info >= (2,3):
- from iri2uri import iri2uri
-else:
- def iri2uri(uri):
- return uri
+from .iri2uri import iri2uri
-def has_timeout(timeout): # python 2.6
+def has_timeout(timeout):
if hasattr(socket, '_GLOBAL_DEFAULT_TIMEOUT'):
return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT)
return (timeout is not None)
__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error',
- 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent',
- 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError',
- 'debuglevel']
+ 'RedirectMissingLocation', 'RedirectLimit',
+ 'FailedToDecompressContent', 'UnimplementedDigestAuthOptionError',
+ 'UnimplementedHmacDigestAuthOptionError',
+ 'debuglevel', 'RETRIES']
# The httplib debug level, set to a non-zero value to get debug output
debuglevel = 0
-
-# Python 2.3 support
-if sys.version_info < (2,4):
- def sorted(seq):
- seq.sort()
- return seq
-
-# Python 2.3 support
-def HTTPResponse__getheaders(self):
- """Return list of (header, value) tuples."""
- if self.msg is None:
- raise httplib.ResponseNotReady()
- return self.msg.items()
-
-if not hasattr(httplib.HTTPResponse, 'getheaders'):
- httplib.HTTPResponse.getheaders = HTTPResponse__getheaders
+# A request will be tried 'RETRIES' times if it fails at the socket/connection level.
+RETRIES = 2
# All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception): pass
-# Some exceptions can be caught and optionally
-# be turned back into responses.
+# Some exceptions can be caught and optionally
+# be turned back into responses.
class HttpLib2ErrorWithResponse(HttpLib2Error):
def __init__(self, desc, response, content):
self.response = response
@@ -122,8 +93,10 @@ class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass
class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
+class MalformedHeader(HttpLib2Error): pass
class RelativeURIError(HttpLib2Error): pass
class ServerNotFoundError(HttpLib2Error): pass
+class CertificateValidationUnsupportedInPython31(HttpLib2Error): pass
# Open Items:
# -----------
@@ -150,10 +123,14 @@ DEFAULT_MAX_REDIRECTS = 5
# Which headers are hop-by-hop headers by default
HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade']
+# Default CA certificates file bundled with httplib2.
+CA_CERTS = os.path.join(
+ os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt")
+
def _get_end2end_headers(response):
hopbyhop = list(HOP_BY_HOP)
hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')])
- return [header for header in response.keys() if header not in hopbyhop]
+ return [header for header in list(response.keys()) if header not in hopbyhop]
URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
@@ -171,7 +148,7 @@ def urlnorm(uri):
raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri)
authority = authority.lower()
scheme = scheme.lower()
- if not path:
+ if not path:
path = "/"
# Could do syntax based normalization of the URI before
# computing the digest. See Section 6.2.2 of Std 66.
@@ -182,8 +159,9 @@ def urlnorm(uri):
# Cache filename construction (original borrowed from Venus http://intertwingly.net/code/venus/)
-re_url_scheme = re.compile(r'^\w+://')
-re_slash = re.compile(r'[?/:|]+')
+re_url_scheme = re.compile(br'^\w+://')
+re_url_scheme_s = re.compile(r'^\w+://')
+re_slash = re.compile(br'[?/:|]+')
def safename(filename):
"""Return a filename suitable for the cache.
@@ -193,37 +171,37 @@ def safename(filename):
"""
try:
- if re_url_scheme.match(filename):
- if isinstance(filename,str):
+ if re_url_scheme_s.match(filename):
+ if isinstance(filename,bytes):
filename = filename.decode('utf-8')
filename = filename.encode('idna')
else:
filename = filename.encode('idna')
except UnicodeError:
pass
- if isinstance(filename,unicode):
+ if isinstance(filename,str):
filename=filename.encode('utf-8')
- filemd5 = _md5(filename).hexdigest()
- filename = re_url_scheme.sub("", filename)
- filename = re_slash.sub(",", filename)
+ filemd5 = _md5(filename).hexdigest().encode('utf-8')
+ filename = re_url_scheme.sub(b"", filename)
+ filename = re_slash.sub(b",", filename)
# limit length of filename
if len(filename)>200:
filename=filename[:200]
- return ",".join((filename, filemd5))
+ return b",".join((filename, filemd5)).decode('utf-8')
NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
def _normalize_headers(headers):
- return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()])
+ return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.items()])
def _parse_cache_control(headers):
retval = {}
- if headers.has_key('cache-control'):
+ if 'cache-control' in headers:
parts = headers['cache-control'].split(',')
parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")]
parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")]
retval = dict(parts_with_args + parts_wo_args)
- return retval
+ return retval
# Whether to use a strict mode to parse WWW-Authenticate headers
# Might lead to bad results in case of ill-formed header value,
@@ -243,26 +221,29 @@ def _parse_www_authenticate(headers, headername='www-authenticate'):
"""Returns a dictionary of dictionaries, one dict
per auth_scheme."""
retval = {}
- if headers.has_key(headername):
- authenticate = headers[headername].strip()
- www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED
- while authenticate:
- # Break off the scheme at the beginning of the line
- if headername == 'authentication-info':
- (auth_scheme, the_rest) = ('digest', authenticate)
- else:
- (auth_scheme, the_rest) = authenticate.split(" ", 1)
- # Now loop over all the key value pairs that come after the scheme,
- # being careful not to roll into the next scheme
- match = www_auth.search(the_rest)
- auth_params = {}
- while match:
- if match and len(match.groups()) == 3:
- (key, value, the_rest) = match.groups()
- auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')])
+ if headername in headers:
+ try:
+ authenticate = headers[headername].strip()
+ www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED
+ while authenticate:
+ # Break off the scheme at the beginning of the line
+ if headername == 'authentication-info':
+ (auth_scheme, the_rest) = ('digest', authenticate)
+ else:
+ (auth_scheme, the_rest) = authenticate.split(" ", 1)
+ # Now loop over all the key value pairs that come after the scheme,
+ # being careful not to roll into the next scheme
match = www_auth.search(the_rest)
- retval[auth_scheme.lower()] = auth_params
- authenticate = the_rest.strip()
+ auth_params = {}
+ while match:
+ if match and len(match.groups()) == 3:
+ (key, value, the_rest) = match.groups()
+ auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')])
+ match = www_auth.search(the_rest)
+ retval[auth_scheme.lower()] = auth_params
+ authenticate = the_rest.strip()
+ except ValueError:
+ raise MalformedHeader("WWW-Authenticate")
return retval
@@ -274,17 +255,17 @@ def _entry_disposition(response_headers, request_headers):
1. Cache-Control: max-stale
2. Age: headers are not used in the calculations.
- Not that this algorithm is simpler than you might think
+ Not that this algorithm is simpler than you might think
because we are operating as a private (non-shared) cache.
This lets us ignore 's-maxage'. We can also ignore
'proxy-invalidate' since we aren't a proxy.
- We will never return a stale document as
- fresh as a design decision, and thus the non-implementation
- of 'max-stale'. This also lets us safely ignore 'must-revalidate'
+ We will never return a stale document as
+ fresh as a design decision, and thus the non-implementation
+ of 'max-stale'. This also lets us safely ignore 'must-revalidate'
since we operate as if every server has sent 'must-revalidate'.
Since we are private we get to ignore both 'public' and
'private' parameters. We also ignore 'no-transform' since
- we don't do any transformations.
+ we don't do any transformations.
The 'no-store' parameter is handled at a higher level.
So the only Cache-Control parameters we look at are:
@@ -293,52 +274,52 @@ def _entry_disposition(response_headers, request_headers):
max-age
min-fresh
"""
-
+
retval = "STALE"
cc = _parse_cache_control(request_headers)
cc_response = _parse_cache_control(response_headers)
- if request_headers.has_key('pragma') and request_headers['pragma'].lower().find('no-cache') != -1:
+ if 'pragma' in request_headers and request_headers['pragma'].lower().find('no-cache') != -1:
retval = "TRANSPARENT"
if 'cache-control' not in request_headers:
request_headers['cache-control'] = 'no-cache'
- elif cc.has_key('no-cache'):
+ elif 'no-cache' in cc:
retval = "TRANSPARENT"
- elif cc_response.has_key('no-cache'):
+ elif 'no-cache' in cc_response:
retval = "STALE"
- elif cc.has_key('only-if-cached'):
+ elif 'only-if-cached' in cc:
retval = "FRESH"
- elif response_headers.has_key('date'):
- date = calendar.timegm(email.Utils.parsedate_tz(response_headers['date']))
+ elif 'date' in response_headers:
+ date = calendar.timegm(email.utils.parsedate_tz(response_headers['date']))
now = time.time()
current_age = max(0, now - date)
- if cc_response.has_key('max-age'):
+ if 'max-age' in cc_response:
try:
freshness_lifetime = int(cc_response['max-age'])
except ValueError:
freshness_lifetime = 0
- elif response_headers.has_key('expires'):
- expires = email.Utils.parsedate_tz(response_headers['expires'])
+ elif 'expires' in response_headers:
+ expires = email.utils.parsedate_tz(response_headers['expires'])
if None == expires:
freshness_lifetime = 0
else:
freshness_lifetime = max(0, calendar.timegm(expires) - date)
else:
freshness_lifetime = 0
- if cc.has_key('max-age'):
+ if 'max-age' in cc:
try:
freshness_lifetime = int(cc['max-age'])
except ValueError:
freshness_lifetime = 0
- if cc.has_key('min-fresh'):
+ if 'min-fresh' in cc:
try:
min_fresh = int(cc['min-fresh'])
except ValueError:
min_fresh = 0
- current_age += min_fresh
+ current_age += min_fresh
if freshness_lifetime > current_age:
retval = "FRESH"
- return retval
+ return retval
def _decompressContent(response, new_content):
content = new_content
@@ -346,7 +327,7 @@ def _decompressContent(response, new_content):
encoding = response.get('content-encoding', None)
if encoding in ['gzip', 'deflate']:
if encoding == 'gzip':
- content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read()
+ content = gzip.GzipFile(fileobj=io.BytesIO(new_content)).read()
if encoding == 'deflate':
content = zlib.decompress(content)
response['content-length'] = str(len(content))
@@ -358,15 +339,32 @@ def _decompressContent(response, new_content):
raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content)
return content
+def _bind_write_headers(msg):
+ from email.header import Header
+ def _write_headers(self):
+ # Self refers to the Generator object
+ for h, v in msg.items():
+ print('%s:' % h, end=' ', file=self._fp)
+ if isinstance(v, Header):
+ print(v.encode(maxlinelen=self._maxheaderlen), file=self._fp)
+ else:
+ # Header's got lots of smarts, so use it.
+ header = Header(v, maxlinelen=self._maxheaderlen, charset='utf-8',
+ header_name=h)
+ print(header.encode(), file=self._fp)
+ # A blank line always separates headers from body
+ print(file=self._fp)
+ return _write_headers
+
def _updateCache(request_headers, response_headers, content, cache, cachekey):
if cachekey:
cc = _parse_cache_control(request_headers)
cc_response = _parse_cache_control(response_headers)
- if cc.has_key('no-store') or cc_response.has_key('no-store'):
+ if 'no-store' in cc or 'no-store' in cc_response:
cache.delete(cachekey)
else:
- info = email.Message.Message()
- for key, value in response_headers.iteritems():
+ info = email.message.Message()
+ for key, value in response_headers.items():
if key not in ['status','content-encoding','transfer-encoding']:
info[key] = value
@@ -386,27 +384,31 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey):
if status == 304:
status = 200
- status_header = 'status: %d\r\n' % response_headers.status
+ status_header = 'status: %d\r\n' % status
- header_str = info.as_string()
+ try:
+ header_str = info.as_string()
+ except UnicodeEncodeError:
+ setattr(info, '_write_headers', _bind_write_headers(info))
+ header_str = info.as_string()
header_str = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", header_str)
- text = "".join([status_header, header_str, content])
+ text = b"".join([status_header.encode('utf-8'), header_str.encode('utf-8'), content])
cache.set(cachekey, text)
def _cnonce():
- dig = _md5("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).hexdigest()
+ dig = _md5(("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).encode('utf-8')).hexdigest()
return dig[:16]
def _wsse_username_token(cnonce, iso_now, password):
- return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip()
+ return base64.b64encode(_sha(("%s%s%s" % (cnonce, iso_now, password)).encode('utf-8')).digest()).strip()
-# For credentials we need two things, first
+# For credentials we need two things, first
# a pool of credential to try (not necesarily tied to BAsic, Digest, etc.)
# Then we also need a list of URIs that have already demanded authentication
-# That list is tricky since sub-URIs can take the same auth, or the
+# That list is tricky since sub-URIs can take the same auth, or the
# auth scheme may change as you descend the tree.
# So we also need each Auth instance to be able to tell us
# how close to the 'top' it is.
@@ -438,11 +440,31 @@ class Authentication(object):
or such returned from the last authorized response.
Over-rise this in sub-classes if necessary.
- Return TRUE is the request is to be retried, for
+ Return TRUE is the request is to be retried, for
example Digest may return stale=true.
"""
return False
+ def __eq__(self, auth):
+ return False
+
+ def __ne__(self, auth):
+ return True
+
+ def __lt__(self, auth):
+ return True
+
+ def __gt__(self, auth):
+ return False
+
+ def __le__(self, auth):
+ return True
+
+ def __ge__(self, auth):
+ return False
+
+ def __bool__(self):
+ return True
class BasicAuthentication(Authentication):
@@ -452,11 +474,11 @@ class BasicAuthentication(Authentication):
def request(self, method, request_uri, headers, content):
"""Modify the request headers to add the appropriate
Authorization header."""
- headers['authorization'] = 'Basic ' + base64.b64encode("%s:%s" % self.credentials).strip()
+ headers['authorization'] = 'Basic ' + base64.b64encode(("%s:%s" % self.credentials).encode('utf-8')).strip().decode('utf-8')
class DigestAuthentication(Authentication):
- """Only do qop='auth' and MD5, since that
+ """Only do qop='auth' and MD5, since that
is all Apache currently implements"""
def __init__(self, credentials, host, request_uri, headers, response, content, http):
Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
@@ -469,46 +491,47 @@ class DigestAuthentication(Authentication):
self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper()
if self.challenge['algorithm'] != 'MD5':
raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm']))
- self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]])
+ self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]])
self.challenge['nc'] = 1
def request(self, method, request_uri, headers, content, cnonce = None):
"""Modify the request headers"""
- H = lambda x: _md5(x).hexdigest()
+ H = lambda x: _md5(x.encode('utf-8')).hexdigest()
KD = lambda s, d: H("%s:%s" % (s, d))
A2 = "".join([method, ":", request_uri])
- self.challenge['cnonce'] = cnonce or _cnonce()
- request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'],
- '%08x' % self.challenge['nc'],
- self.challenge['cnonce'],
- self.challenge['qop'], H(A2)
- ))
- headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % (
- self.credentials[0],
+ self.challenge['cnonce'] = cnonce or _cnonce()
+ request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (
+ self.challenge['nonce'],
+ '%08x' % self.challenge['nc'],
+ self.challenge['cnonce'],
+ self.challenge['qop'], H(A2)))
+ headers['authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % (
+ self.credentials[0],
self.challenge['realm'],
self.challenge['nonce'],
- request_uri,
+ request_uri,
self.challenge['algorithm'],
request_digest,
self.challenge['qop'],
self.challenge['nc'],
- self.challenge['cnonce'],
- )
+ self.challenge['cnonce'])
+ if self.challenge.get('opaque'):
+ headers['authorization'] += ', opaque="%s"' % self.challenge['opaque']
self.challenge['nc'] += 1
def response(self, response, content):
- if not response.has_key('authentication-info'):
+ if 'authentication-info' not in response:
challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {})
if 'true' == challenge.get('stale'):
self.challenge['nonce'] = challenge['nonce']
- self.challenge['nc'] = 1
+ self.challenge['nc'] = 1
return True
else:
updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {})
- if updated_challenge.has_key('nextnonce'):
+ if 'nextnonce' in updated_challenge:
self.challenge['nonce'] = updated_challenge['nextnonce']
- self.challenge['nc'] = 1
+ self.challenge['nc'] = 1
return False
@@ -542,9 +565,8 @@ class HmacDigestAuthentication(Authentication):
else:
self.pwhashmod = _sha
self.key = "".join([self.credentials[0], ":",
- self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(),
- ":", self.challenge['realm']
- ])
+ self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(),
+ ":", self.challenge['realm']])
self.key = self.pwhashmod.new(self.key).hexdigest().lower()
def request(self, method, request_uri, headers, content):
@@ -556,16 +578,15 @@ class HmacDigestAuthentication(Authentication):
cnonce = _cnonce()
request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val)
request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower()
- headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % (
- self.credentials[0],
+ headers['authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % (
+ self.credentials[0],
self.challenge['realm'],
self.challenge['snonce'],
cnonce,
- request_uri,
+ request_uri,
created,
request_digest,
- keylist,
- )
+ keylist)
def response(self, response, content):
challenge = _parse_www_authenticate(response, 'www-authenticate').get('hmacdigest', {})
@@ -578,7 +599,7 @@ class WsseAuthentication(Authentication):
"""This is thinly tested and should not be relied upon.
At this time there isn't any third party server to test against.
Blogger and TypePad implemented this algorithm at one point
- but Blogger has since switched to Basic over HTTPS and
+ but Blogger has since switched to Basic over HTTPS and
TypePad has implemented it wrong, by never issuing a 401
challenge but instead requiring your client to telepathically know that
their endpoint is expecting WSSE profile="UsernameToken"."""
@@ -588,7 +609,7 @@ class WsseAuthentication(Authentication):
def request(self, method, request_uri, headers, content):
"""Modify the request headers to add the appropriate
Authorization header."""
- headers['Authorization'] = 'WSSE profile="UsernameToken"'
+ headers['authorization'] = 'WSSE profile="UsernameToken"'
iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
cnonce = _cnonce()
password_digest = _wsse_username_token(cnonce, iso_now, self.credentials[1])
@@ -600,7 +621,7 @@ class WsseAuthentication(Authentication):
class GoogleLoginAuthentication(Authentication):
def __init__(self, credentials, host, request_uri, headers, response, content, http):
- from urllib import urlencode
+ from urllib.parse import urlencode
Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
challenge = _parse_www_authenticate(response, 'www-authenticate')
service = challenge['googlelogin'].get('service', 'xapi')
@@ -624,7 +645,7 @@ class GoogleLoginAuthentication(Authentication):
def request(self, method, request_uri, headers, content):
"""Modify the request headers to add the appropriate
Authorization header."""
- headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
+ headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
AUTH_SCHEME_CLASSES = {
@@ -639,20 +660,20 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"]
class FileCache(object):
"""Uses a local directory as a store for cached files.
- Not really safe to use if multiple threads or processes are going to
+ Not really safe to use if multiple threads or processes are going to
be running on the same cache.
"""
def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior
self.cache = cache
self.safe = safe
- if not os.path.exists(cache):
+ if not os.path.exists(cache):
os.makedirs(self.cache)
def get(self, key):
retval = None
cacheFullPath = os.path.join(self.cache, self.safe(key))
try:
- f = file(cacheFullPath, "rb")
+ f = open(cacheFullPath, "rb")
retval = f.read()
f.close()
except IOError:
@@ -661,7 +682,7 @@ class FileCache(object):
def set(self, key, value):
cacheFullPath = os.path.join(self.cache, self.safe(key))
- f = file(cacheFullPath, "wb")
+ f = open(cacheFullPath, "wb")
f.write(value)
f.close()
@@ -683,7 +704,7 @@ class Credentials(object):
def iter(self, domain):
for (cdomain, name, password) in self.credentials:
if cdomain == "" or domain == cdomain:
- yield (name, password)
+ yield (name, password)
class KeyCerts(Credentials):
"""Identical to Credentials except that
@@ -703,98 +724,160 @@ p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost', proxy_po
def astuple(self):
return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns,
- self.proxy_user, self.proxy_pass)
+ self.proxy_user, self.proxy_pass)
def isgood(self):
return socks and (self.proxy_host != None) and (self.proxy_port != None)
-class HTTPConnectionWithTimeout(httplib.HTTPConnection):
- """HTTPConnection subclass that supports timeouts"""
+def proxy_info_from_environment(method='http'):
+ """
+ Read proxy info from the environment variables.
+ """
+ if method not in ('http', 'https'):
+ return
- def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None):
- httplib.HTTPConnection.__init__(self, host, port, strict)
- self.timeout = timeout
- self.proxy_info = proxy_info
+ env_var = method + '_proxy'
+ url = os.environ.get(env_var, os.environ.get(env_var.upper()))
+ if not url:
+ return
+ return proxy_info_from_url(url, method)
- def connect(self):
- """Connect to the host and port specified in __init__."""
- # Mostly verbatim from httplib.py.
- msg = "getaddrinfo returns an empty list"
- for res in socket.getaddrinfo(self.host, self.port, 0,
- socket.SOCK_STREAM):
- af, socktype, proto, canonname, sa = res
- try:
- if self.proxy_info and self.proxy_info.isgood():
- self.sock = socks.socksocket(af, socktype, proto)
- self.sock.setproxy(*self.proxy_info.astuple())
- else:
- self.sock = socket.socket(af, socktype, proto)
- # Different from httplib: support timeouts.
- if has_timeout(self.timeout):
- self.sock.settimeout(self.timeout)
- # End of difference from httplib.
- if self.debuglevel > 0:
- print "connect: (%s, %s)" % (self.host, self.port)
-
- self.sock.connect(sa)
- except socket.error, msg:
- if self.debuglevel > 0:
- print 'connect fail:', (self.host, self.port)
- if self.sock:
- self.sock.close()
- self.sock = None
- continue
- break
- if not self.sock:
- raise socket.error, msg
-class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
- "This class allows communication via SSL."
+def proxy_info_from_url(url, method='http'):
+ """
+ Construct a ProxyInfo from a URL (such as http_proxy env var)
+ """
+ url = urllib.parse.urlparse(url)
+ username = None
+ password = None
+ port = None
+ if '@' in url[1]:
+ ident, host_port = url[1].split('@', 1)
+ if ':' in ident:
+ username, password = ident.split(':', 1)
+ else:
+ password = ident
+ else:
+ host_port = url[1]
+ if ':' in host_port:
+ host, port = host_port.split(':', 1)
+ else:
+ host = host_port
+
+ if port:
+ port = int(port)
+ else:
+ port = dict(https=443, http=80)[method]
+
+ proxy_type = 3 # socks.PROXY_TYPE_HTTP
+ return ProxyInfo(
+ proxy_type = proxy_type,
+ proxy_host = host,
+ proxy_port = port,
+ proxy_user = username or None,
+ proxy_pass = password or None,
+ )
+
+
+class HTTPConnectionWithTimeout(http.client.HTTPConnection):
+ """HTTPConnection subclass that supports timeouts
+
+ HTTPConnection subclass that supports timeouts
+
+ All timeouts are in seconds. If None is passed for timeout then
+ Python's default timeout for sockets will be used. See for example
+ the docs of socket.setdefaulttimeout():
+ http://docs.python.org/library/socket.html#socket.setdefaulttimeout
+ """
- def __init__(self, host, port=None, key_file=None, cert_file=None,
- strict=None, timeout=None, proxy_info=None):
- httplib.HTTPSConnection.__init__(self, host, port=port, key_file=key_file,
- cert_file=cert_file, strict=strict)
- self.timeout = timeout
+ def __init__(self, host, port=None, timeout=None, proxy_info=None):
+ http.client.HTTPConnection.__init__(self, host, port=port,
+ timeout=timeout)
self.proxy_info = proxy_info
- def connect(self):
- "Connect to a host on a given (SSL) port."
- if self.proxy_info and self.proxy_info.isgood():
- sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
- sock.setproxy(*self.proxy_info.astuple())
- else:
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
- if has_timeout(self.timeout):
- sock.settimeout(self.timeout)
- sock.connect((self.host, self.port))
- self.sock =_ssl_wrap_socket(sock, self.key_file, self.cert_file)
+class HTTPSConnectionWithTimeout(http.client.HTTPSConnection):
+ """
+ This class allows communication via SSL.
+ All timeouts are in seconds. If None is passed for timeout then
+ Python's default timeout for sockets will be used. See for example
+ the docs of socket.setdefaulttimeout():
+ http://docs.python.org/library/socket.html#socket.setdefaulttimeout
+ """
+ def __init__(self, host, port=None, key_file=None, cert_file=None,
+ timeout=None, proxy_info=None,
+ ca_certs=None, disable_ssl_certificate_validation=False):
+ self.proxy_info = proxy_info
+ context = None
+ if ca_certs is None:
+ ca_certs = CA_CERTS
+ if (cert_file or ca_certs) and not disable_ssl_certificate_validation:
+ if not hasattr(ssl, 'SSLContext'):
+ raise CertificateValidationUnsupportedInPython31()
+ context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
+ context.verify_mode = ssl.CERT_REQUIRED
+ if cert_file:
+ context.load_cert_chain(cert_file, key_file)
+ if ca_certs:
+ context.load_verify_locations(ca_certs)
+ http.client.HTTPSConnection.__init__(
+ self, host, port=port, key_file=key_file,
+ cert_file=cert_file, timeout=timeout, context=context,
+ check_hostname=True)
+
+
+SCHEME_TO_CONNECTION = {
+ 'http': HTTPConnectionWithTimeout,
+ 'https': HTTPSConnectionWithTimeout,
+}
class Http(object):
"""An HTTP client that handles:
-- all methods
-- caching
-- ETags
-- compression,
-- HTTPS
-- Basic
-- Digest
-- WSSE
-
-and more.
- """
- def __init__(self, cache=None, timeout=None, proxy_info=None):
- """The value of proxy_info is a ProxyInfo instance.
-If 'cache' is a string then it is used as a directory name
-for a disk cache. Otherwise it must be an object that supports
-the same interface as FileCache."""
+ - all methods
+ - caching
+ - ETags
+ - compression,
+ - HTTPS
+ - Basic
+ - Digest
+ - WSSE
+
+ and more.
+ """
+ def __init__(self, cache=None, timeout=None,
+ proxy_info=proxy_info_from_environment,
+ ca_certs=None, disable_ssl_certificate_validation=False):
+ """If 'cache' is a string then it is used as a directory name for
+ a disk cache. Otherwise it must be an object that supports the
+ same interface as FileCache.
+
+ All timeouts are in seconds. If None is passed for timeout
+ then Python's default timeout for sockets will be used. See
+ for example the docs of socket.setdefaulttimeout():
+ http://docs.python.org/library/socket.html#socket.setdefaulttimeout
+
+ `proxy_info` may be:
+ - a callable that takes the http scheme ('http' or 'https') and
+ returns a ProxyInfo instance per request. By default, uses
+ proxy_info_from_environment.
+ - a ProxyInfo instance (static proxy config).
+ - None (proxy disabled).
+
+ ca_certs is the path of a file containing root CA certificates for SSL
+ server certificate validation. By default, a CA cert file bundled with
+ httplib2 is used.
+
+ If disable_ssl_certificate_validation is true, SSL cert validation will
+ not be performed.
+"""
self.proxy_info = proxy_info
+ self.ca_certs = ca_certs
+ self.disable_ssl_certificate_validation = \
+ disable_ssl_certificate_validation
# Map domain name to an httplib connection
self.connections = {}
# The location of the cache, for now a directory
@@ -815,10 +898,10 @@ the same interface as FileCache."""
# If set to False then no redirects are followed, even safe ones.
self.follow_redirects = True
-
+
# Which HTTP methods do we apply optimistic concurrency to, i.e.
# which methods get an "if-match:" etag header added to them.
- self.optimistic_concurrency_methods = ["PUT"]
+ self.optimistic_concurrency_methods = ["PUT", "PATCH"]
# If 'follow_redirects' is True, and this is set to True then
# all redirecs are followed, including unsafe ones.
@@ -826,10 +909,27 @@ the same interface as FileCache."""
self.ignore_etag = False
- self.force_exception_to_status_code = False
+ self.force_exception_to_status_code = False
self.timeout = timeout
+ # Keep Authorization: headers on a redirect.
+ self.forward_authorization_headers = False
+
+ def __getstate__(self):
+ state_dict = copy.copy(self.__dict__)
+ # In case request is augmented by some foreign object such as
+ # credentials which handle auth
+ if 'request' in state_dict:
+ del state_dict['request']
+ if 'connections' in state_dict:
+ del state_dict['connections']
+ return state_dict
+
+ def __setstate__(self, state):
+ self.__dict__.update(state)
+ self.connections = {}
+
def _auth_from_challenge(self, host, request_uri, headers, response, content):
"""A generator that creates Authorization objects
that can be applied to requests.
@@ -837,7 +937,7 @@ the same interface as FileCache."""
challenges = _parse_www_authenticate(response, 'www-authenticate')
for cred in self.credentials.iter(host):
for scheme in AUTH_SCHEME_ORDER:
- if challenges.has_key(scheme):
+ if scheme in challenges:
yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self)
def add_credentials(self, name, password, domain=""):
@@ -857,19 +957,43 @@ the same interface as FileCache."""
self.authorizations = []
def _conn_request(self, conn, request_uri, method, body, headers):
- for i in range(2):
+ for i in range(RETRIES):
try:
+ if conn.sock is None:
+ conn.connect()
conn.request(method, request_uri, body, headers)
+ except socket.timeout:
+ conn.close()
+ raise
except socket.gaierror:
conn.close()
raise ServerNotFoundError("Unable to find the server at %s" % conn.host)
- except (socket.error, httplib.HTTPException):
+ except socket.error as e:
+ errno_ = (e.args[0].errno if isinstance(e.args[0], socket.error) else e.errno)
+ if errno_ == errno.ECONNREFUSED: # Connection refused
+ raise
+ except http.client.HTTPException:
+ if conn.sock is None:
+ if i < RETRIES-1:
+ conn.close()
+ conn.connect()
+ continue
+ else:
+ conn.close()
+ raise
+ if i < RETRIES-1:
+ conn.close()
+ conn.connect()
+ continue
# Just because the server closed the connection doesn't apparently mean
# that the server didn't send a response.
pass
try:
response = conn.getresponse()
- except (socket.error, httplib.HTTPException):
+ except socket.timeout:
+ raise
+ except (socket.error, http.client.HTTPException):
+ conn.close()
if i == 0:
conn.close()
conn.connect()
@@ -877,14 +1001,15 @@ the same interface as FileCache."""
else:
raise
else:
- content = ""
+ content = b""
if method == "HEAD":
- response.close()
+ conn.close()
else:
content = response.read()
response = Response(response)
if method != "HEAD":
content = _decompressContent(response, content)
+
break
return (response, content)
@@ -895,12 +1020,12 @@ the same interface as FileCache."""
auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)]
auth = auths and sorted(auths)[0][1] or None
- if auth:
+ if auth:
auth.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers)
- if auth:
+ if auth:
if auth.response(response, body):
auth.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers )
@@ -908,7 +1033,7 @@ the same interface as FileCache."""
if response.status == 401:
for authorization in self._auth_from_challenge(host, request_uri, headers, response, content):
- authorization.request(method, request_uri, headers, body)
+ authorization.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers, )
if response.status != 401:
self.authorizations.append(authorization)
@@ -920,37 +1045,42 @@ the same interface as FileCache."""
# Pick out the location header and basically start from the beginning
# remembering first to strip the ETag header and decrement our 'depth'
if redirections:
- if not response.has_key('location') and response.status != 300:
+ if 'location' not in response and response.status != 300:
raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content)
# Fix-up relative redirects (which violate an RFC 2616 MUST)
- if response.has_key('location'):
+ if 'location' in response:
location = response['location']
(scheme, authority, path, query, fragment) = parse_uri(location)
if authority == None:
- response['location'] = urlparse.urljoin(absolute_uri, location)
+ response['location'] = urllib.parse.urljoin(absolute_uri, location)
if response.status == 301 and method in ["GET", "HEAD"]:
response['-x-permanent-redirect-url'] = response['location']
- if not response.has_key('content-location'):
- response['content-location'] = absolute_uri
+ if 'content-location' not in response:
+ response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey)
- if headers.has_key('if-none-match'):
+ if 'if-none-match' in headers:
del headers['if-none-match']
- if headers.has_key('if-modified-since'):
+ if 'if-modified-since' in headers:
del headers['if-modified-since']
- if response.has_key('location'):
+ if 'authorization' in headers and not self.forward_authorization_headers:
+ del headers['authorization']
+ if 'location' in response:
location = response['location']
old_response = copy.deepcopy(response)
- if not old_response.has_key('content-location'):
- old_response['content-location'] = absolute_uri
- redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method
+ if 'content-location' not in old_response:
+ old_response['content-location'] = absolute_uri
+ redirect_method = method
+ if response.status in [302, 303]:
+ redirect_method = "GET"
+ body = None
(response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1)
response.previous = old_response
else:
- raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content)
- elif response.status in [200, 203] and method == "GET":
+ raise RedirectLimit("Redirected more times than redirection_limit allows.", response, content)
+ elif response.status in [200, 203] and method in ["GET", "HEAD"]:
# Don't cache 206's since we aren't going to handle byte range requests
- if not response.has_key('content-location'):
- response['content-location'] = absolute_uri
+ if 'content-location' not in response:
+ response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey)
return (response, content)
@@ -965,10 +1095,10 @@ the same interface as FileCache."""
def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None):
""" Performs a single HTTP request.
-The 'uri' is the URI of the HTTP resource and can begin
+The 'uri' is the URI of the HTTP resource and can begin
with either 'http' or 'https'. The value of 'uri' must be an absolute URI.
-The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc.
+The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc.
There is no restriction on the methods allowed.
The 'body' is the entity body to be sent with the request. It is a string
@@ -977,11 +1107,11 @@ object.
Any extra headers that are to be sent with the request should be provided in the
'headers' dictionary.
-The maximum number of redirect to follow before raising an
+The maximum number of redirect to follow before raising an
exception is 'redirections. The default is 5.
-The return value is a tuple of (response, content), the first
-being and instance of the 'Response' class, the second being
+The return value is a tuple of (response, content), the first
+being and instance of the 'Response' class, the second being
a string that contains the response entity body.
"""
try:
@@ -990,8 +1120,8 @@ a string that contains the response entity body.
else:
headers = self._normalize_headers(headers)
- if not headers.has_key('user-agent'):
- headers['user-agent'] = "Python-httplib2/%s" % __version__
+ if 'user-agent' not in headers:
+ headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__
uri = iri2uri(uri)
@@ -1006,43 +1136,54 @@ a string that contains the response entity body.
conn = self.connections[conn_key]
else:
if not connection_type:
- connection_type = (scheme == 'https') and HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout
+ connection_type = SCHEME_TO_CONNECTION[scheme]
certs = list(self.certificates.iter(authority))
- if scheme == 'https' and certs:
- conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0],
- cert_file=certs[0][1], timeout=self.timeout, proxy_info=self.proxy_info)
+ if issubclass(connection_type, HTTPSConnectionWithTimeout):
+ if certs:
+ conn = self.connections[conn_key] = connection_type(
+ authority, key_file=certs[0][0],
+ cert_file=certs[0][1], timeout=self.timeout,
+ proxy_info=self.proxy_info,
+ ca_certs=self.ca_certs,
+ disable_ssl_certificate_validation=
+ self.disable_ssl_certificate_validation)
+ else:
+ conn = self.connections[conn_key] = connection_type(
+ authority, timeout=self.timeout,
+ proxy_info=self.proxy_info,
+ ca_certs=self.ca_certs,
+ disable_ssl_certificate_validation=
+ self.disable_ssl_certificate_validation)
else:
- conn = self.connections[conn_key] = connection_type(authority, timeout=self.timeout, proxy_info=self.proxy_info)
+ conn = self.connections[conn_key] = connection_type(
+ authority, timeout=self.timeout,
+ proxy_info=self.proxy_info)
conn.set_debuglevel(debuglevel)
- if method in ["GET", "HEAD"] and 'range' not in headers and 'accept-encoding' not in headers:
+ if 'range' not in headers and 'accept-encoding' not in headers:
headers['accept-encoding'] = 'gzip, deflate'
- info = email.Message.Message()
+ info = email.message.Message()
cached_value = None
if self.cache:
cachekey = defrag_uri
cached_value = self.cache.get(cachekey)
if cached_value:
- # info = email.message_from_string(cached_value)
- #
- # Need to replace the line above with the kludge below
- # to fix the non-existent bug not fixed in this
- # bug report: http://mail.python.org/pipermail/python-bugs-list/2005-September/030289.html
try:
- info, content = cached_value.split('\r\n\r\n', 1)
- feedparser = email.FeedParser.FeedParser()
- feedparser.feed(info)
- info = feedparser.close()
- feedparser._parse = None
- except IndexError:
+ info, content = cached_value.split(b'\r\n\r\n', 1)
+ info = email.message_from_bytes(info)
+ for k, v in info.items():
+ if v.startswith('=?') and v.endswith('?='):
+ info.replace_header(k,
+ str(*email.header.decode_header(v)[0]))
+ except (IndexError, ValueError):
self.cache.delete(cachekey)
cachekey = None
cached_value = None
else:
cachekey = None
- if method in self.optimistic_concurrency_methods and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers:
+ if method in self.optimistic_concurrency_methods and self.cache and 'etag' in info and not self.ignore_etag and 'if-match' not in headers:
# http://www.w3.org/1999/04/Editing/
headers['if-match'] = info['etag']
@@ -1058,13 +1199,15 @@ a string that contains the response entity body.
for header in vary_headers:
key = '-varied-%s' % header
value = info[key]
- if headers.get(header, '') != value:
+ if headers.get(header, None) != value:
cached_value = None
break
if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers:
- if info.has_key('-x-permanent-redirect-url'):
+ if '-x-permanent-redirect-url' in info:
# Should cached permanent redirects be counted in our redirection count? For now, yes.
+ if redirections <= 0:
+ raise RedirectLimit("Redirected more times than redirection_limit allows.", {}, "")
(response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1)
response.previous = Response(info)
response.previous.fromcache = True
@@ -1072,26 +1215,26 @@ a string that contains the response entity body.
# Determine our course of action:
# Is the cached entry fresh or stale?
# Has the client requested a non-cached response?
- #
- # There seems to be three possible answers:
+ #
+ # There seems to be three possible answers:
# 1. [FRESH] Return the cache entry w/o doing a GET
# 2. [STALE] Do the GET (but add in cache validators if available)
# 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request
- entry_disposition = _entry_disposition(info, headers)
-
+ entry_disposition = _entry_disposition(info, headers)
+
if entry_disposition == "FRESH":
if not cached_value:
info['status'] = '504'
- content = ""
+ content = b""
response = Response(info)
if cached_value:
response.fromcache = True
return (response, content)
if entry_disposition == "STALE":
- if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers:
+ if 'etag' in info and not self.ignore_etag and not 'if-none-match' in headers:
headers['if-none-match'] = info['etag']
- if info.has_key('last-modified') and not 'last-modified' in headers:
+ if 'last-modified' in info and not 'last-modified' in headers:
headers['if-modified-since'] = info['last-modified']
elif entry_disposition == "TRANSPARENT":
pass
@@ -1100,7 +1243,7 @@ a string that contains the response entity body.
if response.status == 304 and method == "GET":
# Rewrite the cache entry with the new end-to-end headers
- # Take all headers that are in response
+ # Take all headers that are in response
# and overwrite their values in info.
# unless they are hop-by-hop, or are listed in the connection header.
@@ -1112,55 +1255,55 @@ a string that contains the response entity body.
_updateCache(headers, merged_response, content, self.cache, cachekey)
response = merged_response
response.status = 200
- response.fromcache = True
+ response.fromcache = True
elif response.status == 200:
content = new_content
else:
self.cache.delete(cachekey)
- content = new_content
- else:
+ content = new_content
+ else:
cc = _parse_cache_control(headers)
- if cc.has_key('only-if-cached'):
+ if 'only-if-cached'in cc:
info['status'] = '504'
response = Response(info)
- content = ""
+ content = b""
else:
(response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
- except Exception, e:
+ except Exception as e:
if self.force_exception_to_status_code:
if isinstance(e, HttpLib2ErrorWithResponse):
response = e.response
content = e.content
response.status = 500
- response.reason = str(e)
+ response.reason = str(e)
elif isinstance(e, socket.timeout):
- content = "Request Timeout"
- response = Response( {
- "content-type": "text/plain",
- "status": "408",
- "content-length": len(content)
- })
+ content = b"Request Timeout"
+ response = Response({
+ "content-type": "text/plain",
+ "status": "408",
+ "content-length": len(content)
+ })
response.reason = "Request Timeout"
else:
- content = str(e)
- response = Response( {
- "content-type": "text/plain",
- "status": "400",
- "content-length": len(content)
- })
- response.reason = "Bad Request"
+ content = str(e).encode('utf-8')
+ response = Response({
+ "content-type": "text/plain",
+ "status": "400",
+ "content-length": len(content)
+ })
+ response.reason = "Bad Request"
else:
raise
-
+
return (response, content)
-
+
class Response(dict):
- """An object more like email.Message than httplib.HTTPResponse."""
-
+ """An object more like email.message than httplib.HTTPResponse."""
+
"""Is this response from our local cache"""
fromcache = False
@@ -1176,27 +1319,31 @@ class Response(dict):
previous = None
def __init__(self, info):
- # info is either an email.Message or
+ # info is either an email.message or
# an httplib.HTTPResponse object.
- if isinstance(info, httplib.HTTPResponse):
- for key, value in info.getheaders():
- self[key.lower()] = value
+ if isinstance(info, http.client.HTTPResponse):
+ for key, value in info.getheaders():
+ key = key.lower()
+ prev = self.get(key)
+ if prev is not None:
+ value = ', '.join((prev, value))
+ self[key] = value
self.status = info.status
self['status'] = str(self.status)
self.reason = info.reason
self.version = info.version
- elif isinstance(info, email.Message.Message):
- for key, value in info.items():
- self[key] = value
+ elif isinstance(info, email.message.Message):
+ for key, value in list(info.items()):
+ self[key.lower()] = value
self.status = int(self['status'])
else:
- for key, value in info.iteritems():
- self[key] = value
+ for key, value in info.items():
+ self[key.lower()] = value
self.status = int(self.get('status', self.status))
def __getattr__(self, name):
if name == 'dict':
- return self
- else:
- raise AttributeError, name
+ return self
+ else:
+ raise AttributeError(name)

Powered by Google App Engine
This is Rietveld 408576698