Mailinglist Archive: opensuse-commit (1826 mails)
| < Previous | Next > |
commit python-httplib2
- From: root@xxxxxxxxxxxxxxx (h_root)
- Date: Tue, 09 Sep 2008 22:19:40 +0200
- Message-id: <20080909201941.456F867815E@xxxxxxxxxxxxxxx>
Hello community,
here is the log from the commit of package python-httplib2
checked in at Tue Sep 9 22:19:40 CEST 2008.
--------
--- python-httplib2/python-httplib2.changes 2006-10-21 22:37:19.000000000
+0200
+++ /mounts/work_src_done/STABLE/python-httplib2/python-httplib2.changes
2008-09-09 21:12:06.458442000 +0200
@@ -1,0 +2,12 @@
+Tue Sep 9 12:41:28 CEST 2008 - cstender@xxxxxxx
+
+- updated package to version 0.4.0
+ o added support for proxies if the Socksipy module is installed
+ o fixed bug with some HEAD responses having content-length set to
+ zero incorrectly
+ o fixed most except's to catch a specific exception
+ o added 'connection_type' parameter to Http.request()
+ o the default for 'force_exception_to_status_code' was changed to
+ False
+
+-------------------------------------------------------------------
Old:
----
httplib2-0.2.0.tar.gz
New:
----
httplib2-0.4.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-httplib2.spec ++++++
--- /var/tmp/diff_new_pack.LN7082/_old 2008-09-09 22:19:33.000000000 +0200
+++ /var/tmp/diff_new_pack.LN7082/_new 2008-09-09 22:19:33.000000000 +0200
@@ -1,27 +1,34 @@
#
-# spec file for package python-httplib2 (Version 0.2.0)
+# spec file for package python-httplib2 (Version 0.4.0)
#
-# Copyright (c) 2006 SUSE LINUX Products GmbH, Nuernberg, Germany.
-# This file and all modifications and additions to the pristine
-# package are under the same license as the package itself.
+# Copyright (c) 2008 SUSE LINUX Products GmbH, Nuernberg, Germany.
#
+# All modifications and additions to the file contributed by third parties
+# remain the property of their copyright owners, unless otherwise agreed
+# upon. The license for this file, and modifications and additions to the
+# file, is the same license as for the pristine package itself (unless the
+# license for the pristine package is not an Open Source License, in which
+# case the license is the MIT License). An "Open Source License" is a
+# license that conforms to the Open Source Definition (Version 1.9)
+# published by the Open Source Initiative.
+
# Please submit bugfixes or comments via http://bugs.opensuse.org/
#
# norootforbuild
+
Name: python-httplib2
BuildRequires: python-devel
Summary: A Python HTTP library
-Version: 0.2.0
+Version: 0.4.0
Release: 1
Source0: httplib2-%{version}.tar.gz
License: X11/MIT
Group: Development/Libraries/Python
BuildRoot: %{_tmppath}/%{name}-%{version}-build
-Prefix: %{_prefix}
-URL: http://bitworking.org/projects/httplib2/
-
+Url: http://bitworking.org/projects/httplib2/
+%define prefix /usr
%py_requires
%description
@@ -51,6 +58,15 @@
%defattr(-,root,root)
%doc PKG-INFO README
-%changelog -n python-httplib2
-* Sat Oct 21 2006 - dgollub@xxxxxxx
+%changelog
+* Tue Sep 09 2008 cstender@xxxxxxx
+- updated package to version 0.4.0
+ o added support for proxies if the Socksipy module is installed
+ o fixed bug with some HEAD responses having content-length set to
+ zero incorrectly
+ o fixed most except's to catch a specific exception
+ o added 'connection_type' parameter to Http.request()
+ o the default for 'force_exception_to_status_code' was changed to
+ False
+* Sun Oct 22 2006 dgollub@xxxxxxx
- initial version of python-httplib2 (0.2.0)
++++++ httplib2-0.2.0.tar.gz -> httplib2-0.4.0.tar.gz ++++++
diff -urN --exclude=CVS --exclude=.cvsignore --exclude=.svn
--exclude=.svnignore old/httplib2-0.2.0/httplib2/__init__.py
new/httplib2-0.4.0/httplib2/__init__.py
--- old/httplib2-0.2.0/httplib2/__init__.py 2006-07-01 07:53:26.000000000
+0200
+++ new/httplib2-0.4.0/httplib2/__init__.py 2007-10-23 17:25:46.000000000
+0200
@@ -1,3 +1,4 @@
+from __future__ import generators
"""
httplib2
@@ -6,6 +7,9 @@
Requires Python 2.3 or later
+Changelog:
+2007-08-18, Rick: Modified so it's able to use a socks proxy if needed.
+
"""
__author__ = "Joe Gregorio (joe@xxxxxxxxxxxxxx)"
@@ -14,13 +18,18 @@
"James Antill",
"Xavier Verges Farrero",
"Jonathan Feinberg",
- "Blair Zajac"]
+ "Blair Zajac",
+ "Sam Ruby",
+ "Louis Nyffenegger"]
__license__ = "MIT"
-__version__ = "$Rev: 196 $"
+__version__ = "$Rev: 259 $"
import re
+import sys
import md5
-import rfc822
+import email
+import email.Utils
+import email.Message
import StringIO
import gzip
import zlib
@@ -35,17 +44,30 @@
import sha
import hmac
from gettext import gettext as _
+import socket
+
+try:
+ import socks
+except ImportError:
+ socks = None
+
+if sys.version_info >= (2,3):
+ from iri2uri import iri2uri
+else:
+ def iri2uri(uri):
+ return uri
-__all__ = ['Http', 'Response', 'HttpLib2Error',
+__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error',
'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent',
- 'UnimplementedDigestAuthOptionError',
'UnimplementedHmacDigestAuthOptionError']
+ 'UnimplementedDigestAuthOptionError',
'UnimplementedHmacDigestAuthOptionError',
+ 'debuglevel']
# The httplib debug level, set to a non-zero value to get debug output
debuglevel = 0
# Python 2.3 support
-if 'sorted' not in __builtins__:
+if sys.version_info < (2,4):
def sorted(seq):
seq.sort()
return seq
@@ -54,7 +76,6 @@
def HTTPResponse__getheaders(self):
"""Return list of (header, value) tuples."""
if self.msg is None:
- print "================================"
raise httplib.ResponseNotReady()
return self.msg.items()
@@ -64,11 +85,22 @@
# All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception): pass
-class RedirectMissingLocation(HttpLib2Error): pass
-class RedirectLimit(HttpLib2Error): pass
-class FailedToDecompressContent(HttpLib2Error): pass
-class UnimplementedDigestAuthOptionError(HttpLib2Error): pass
-class UnimplementedHmacDigestAuthOptionError(HttpLib2Error): pass
+# Some exceptions can be caught and optionally
+# be turned back into responses.
+class HttpLib2ErrorWithResponse(HttpLib2Error):
+ def __init__(self, desc, response, content):
+ self.response = response
+ self.content = content
+ HttpLib2Error.__init__(self, desc)
+
+class RedirectMissingLocation(HttpLib2ErrorWithResponse): pass
+class RedirectLimit(HttpLib2ErrorWithResponse): pass
+class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass
+class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
+class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
+
+class RelativeURIError(HttpLib2Error): pass
+class ServerNotFoundError(HttpLib2Error): pass
# Open Items:
# -----------
@@ -110,6 +142,53 @@
groups = URI.match(uri).groups()
return (groups[1], groups[3], groups[4], groups[6], groups[8])
+def urlnorm(uri):
+ (scheme, authority, path, query, fragment) = parse_uri(uri)
+ if not scheme or not authority:
+ raise RelativeURIError("Only absolute URIs are allowed. uri = %s" %
uri)
+ authority = authority.lower()
+ scheme = scheme.lower()
+ if not path:
+ path = "/"
+ # Could do syntax based normalization of the URI before
+ # computing the digest. See Section 6.2.2 of Std 66.
+ request_uri = query and "?".join([path, query]) or path
+ scheme = scheme.lower()
+ defrag_uri = scheme + "://" + authority + request_uri
+ return scheme, authority, request_uri, defrag_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'[?/:|]+')
+
+def safename(filename):
+ """Return a filename suitable for the cache.
+
+ Strips dangerous and common characters to create a filename we
+ can use to store the cache in.
+ """
+
+ try:
+ if re_url_scheme.match(filename):
+ if isinstance(filename,str):
+ filename = filename.decode('utf-8')
+ filename = filename.encode('idna')
+ else:
+ filename = filename.encode('idna')
+ except UnicodeError:
+ pass
+ if isinstance(filename,unicode):
+ filename=filename.encode('utf-8')
+ filemd5 = md5.new(filename).hexdigest()
+ filename = re_url_scheme.sub("", filename)
+ filename = re_slash.sub(",", filename)
+
+ # limit length of filename
+ if len(filename)>200:
+ filename=filename[:200]
+ return ",".join((filename, filemd5))
+
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()])
@@ -207,20 +286,33 @@
elif cc.has_key('only-if-cached'):
retval = "FRESH"
elif response_headers.has_key('date'):
- date = calendar.timegm(rfc822.parsedate_tz(response_headers['date']))
+ 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'):
- freshness_lifetime = int(cc_response['max-age'])
+ try:
+ freshness_lifetime = int(cc_response['max-age'])
+ except ValueError:
+ freshness_lifetime = 0
elif response_headers.has_key('expires'):
- expires = rfc822.parsedate_tz(response_headers['expires'])
- freshness_lifetime = max(0, calendar.timegm(expires) - date)
+ 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'):
- freshness_lifetime = min(freshness_lifetime, int(cc['max-age']))
+ try:
+ freshness_lifetime = int(cc['max-age'])
+ except ValueError:
+ freshness_lifetime = 0
if cc.has_key('min-fresh'):
- current_age += int(cc['min-fresh'])
+ try:
+ min_fresh = int(cc['min-fresh'])
+ except ValueError:
+ min_fresh = 0
+ current_age += min_fresh
if freshness_lifetime > current_age:
retval = "FRESH"
return retval
@@ -228,13 +320,17 @@
def _decompressContent(response, new_content):
content = new_content
try:
- if response.get('content-encoding', None) == 'gzip':
- content =
gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read()
- if response.get('content-encoding', None) == 'deflate':
- content = zlib.decompress(content)
- except:
+ encoding = response.get('content-encoding', None)
+ if encoding in ['gzip', 'deflate']:
+ if encoding == 'gzip':
+ content =
gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read()
+ if encoding == 'deflate':
+ content = zlib.decompress(content)
+ response['content-length'] = str(len(content))
+ del response['content-encoding']
+ except IOError:
content = ""
- raise FailedToDecompressContent(_("Content purported to be compressed
with %s but failed to decompress.") % response.get('content-encoding'))
+ raise FailedToDecompressContent(_("Content purported to be compressed
with %s but failed to decompress.") % response.get('content-encoding'),
response, content)
return content
def _updateCache(request_headers, response_headers, content, cache, cachekey):
@@ -244,14 +340,23 @@
if cc.has_key('no-store') or cc_response.has_key('no-store'):
cache.delete(cachekey)
else:
- f = StringIO.StringIO("")
- info = rfc822.Message(StringIO.StringIO(""))
+ info = email.Message.Message()
for key, value in response_headers.iteritems():
- info[key] = value
- f.write(str(info))
- f.write("\r\n\r\n")
- f.write(content)
- cache.set(cachekey, f.getvalue())
+ if key not in
['status','content-encoding','transfer-encoding']:
+ info[key] = value
+
+ status = response_headers.status
+ if status == 304:
+ status = 200
+
+ status_header = 'status: %d\r\n' % response_headers.status
+
+ header_str = info.as_string()
+
+ header_str = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", header_str)
+ text = "".join([status_header, header_str, content])
+
+ cache.set(cachekey, text)
def _cnonce():
dig = md5.new("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0,
9)] for i in range(20)])).hexdigest()
@@ -269,7 +374,7 @@
# So we also need each Auth instance to be able to tell us
# how close to the 'top' it is.
-class Authentication:
+class Authentication(object):
def __init__(self, credentials, host, request_uri, headers, response,
content, http):
(scheme, authority, path, query, fragment) = parse_uri(request_uri)
self.path = path
@@ -356,13 +461,13 @@
def response(self, response, content):
if not response.has_key('authentication-info'):
- challenge = _parse_www_authenticate(response,
'www-authenticate')['digest']
+ challenge = _parse_www_authenticate(response,
'www-authenticate').get('digest', {})
if 'true' == challenge.get('stale'):
self.challenge['nonce'] = challenge['nonce']
self.challenge['nc'] = 1
return True
else:
- updated_challenge = _parse_www_authenticate(response,
'authentication-info')['digest']
+ updated_challenge = _parse_www_authenticate(response,
'authentication-info').get('digest', {})
if updated_challenge.has_key('nextnonce'):
self.challenge['nonce'] = updated_challenge['nextnonce']
@@ -378,7 +483,6 @@
Authentication.__init__(self, credentials, host, request_uri, headers,
response, content, http)
challenge = _parse_www_authenticate(response, 'www-authenticate')
self.challenge = challenge['hmacdigest']
- print self.challenge
# TODO: self.challenge['domain']
self.challenge['reason'] = self.challenge.get('reason', 'unauthorized')
if self.challenge['reason'] not in ['unauthorized', 'integrity']:
@@ -404,9 +508,6 @@
self.pwhashmod.new("".join([self.credentials[1],
self.challenge['salt']])).hexdigest().lower(),
":", self.challenge['realm']
])
- print response['www-authenticate']
- print "".join([self.credentials[1], self.challenge['salt']])
- print "key_str = %s" % self.key
self.key = self.pwhashmod.new(self.key).hexdigest().lower()
def request(self, method, request_uri, headers, content):
@@ -417,8 +518,6 @@
created = time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
cnonce = _cnonce()
request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce,
self.challenge['snonce'], headers_val)
- print "key = %s" % self.key
- print "msg = %s" % request_digest
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],
@@ -466,8 +565,17 @@
def __init__(self, credentials, host, request_uri, headers, response,
content, http):
from urllib 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')
+ # Bloggger actually returns the service in the challenge
+ # For the rest we guess based on the URI
+ if service == 'xapi' and request_uri.find("calendar") > 0:
+ service = "cl"
+ # No point in guessing Base or Spreadsheet
+ #elif request_uri.find("spreadsheets") > 0:
+ # service = "wise"
- auth = dict(Email=credentials[0], Passwd=credentials[1], service='cl',
source=headers['user-agent'])
+ auth = dict(Email=credentials[0], Passwd=credentials[1],
service=service, source=headers['user-agent'])
resp, content =
self.http.request("https://www.google.com/accounts/ClientLogin", method="POST",
body=urlencode(auth), headers={'Content-Type':
'application/x-www-form-urlencoded'})
lines = content.split('\n')
d = dict([tuple(line.split("=", 1)) for line in lines if line])
@@ -492,45 +600,166 @@
AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"]
+def _md5(s):
+ return
-class FileCache:
+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
be running on the same cache.
"""
- def __init__(self, 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):
os.makedirs(self.cache)
def get(self, key):
retval = None
- cacheFullPath = os.path.join(self.cache, key)
+ cacheFullPath = os.path.join(self.cache, self.safe(key))
try:
f = file(cacheFullPath, "r")
retval = f.read()
f.close()
- except:
+ except IOError:
pass
return retval
def set(self, key, value):
- cacheFullPath = os.path.join(self.cache, key)
+ cacheFullPath = os.path.join(self.cache, self.safe(key))
f = file(cacheFullPath, "w")
f.write(value)
f.close()
def delete(self, key):
- cacheFullPath = os.path.join(self.cache, key)
+ cacheFullPath = os.path.join(self.cache, self.safe(key))
if os.path.exists(cacheFullPath):
os.remove(cacheFullPath)
-class Http:
- """An HTTP client that handles all
- methods, caching, ETags, compression,
- HTTPS, Basic, Digest, WSSE, etc.
+class Credentials(object):
+ def __init__(self):
+ self.credentials = []
+
+ def add(self, name, password, domain=""):
+ self.credentials.append((domain.lower(), name, password))
+
+ def clear(self):
+ self.credentials = []
+
+ def iter(self, domain):
+ for (cdomain, name, password) in self.credentials:
+ if cdomain == "" or domain == cdomain:
+ yield (name, password)
+
+class KeyCerts(Credentials):
+ """Identical to Credentials except that
+ name/password are mapped to key/cert."""
+ pass
+
+
+class ProxyInfo(object):
+ """Collect information required to use a proxy."""
+ def __init__(self, proxy_type, proxy_host, proxy_port, proxy_rdns=None,
proxy_user=None, proxy_pass=None):
+ """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX
+ constants. For example:
+
+p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost',
proxy_port=8000)
+ """
+ self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns,
self.proxy_user, self.proxy_pass = proxy_type, proxy_host, proxy_port,
proxy_rdns, proxy_user, proxy_pass
+
+ def astuple(self):
+ return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns,
+ 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 __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
+
+ 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 self.timeout is not None:
+ 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 __init__(self, host, port=None, key_file=None, cert_file=None,
+ strict=None, timeout=None, proxy_info=None):
+ self.timeout = timeout
+ self.proxy_info = proxy_info
+ httplib.HTTPSConnection.__init__(self, host, port=port,
key_file=key_file,
+ cert_file=cert_file, strict=strict)
+
+ def connect(self):
+ "Connect to a host on a given (SSL) port."
+
+ if self.proxy_info and self.proxy_info.isgood():
+ self.sock.setproxy(*self.proxy_info.astuple())
+ sock.setproxy(*self.proxy_info.astuple())
+ else:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ if self.timeout is not None:
+ sock.settimeout(self.timeout)
+ sock.connect((self.host, self.port))
+ ssl = socket.ssl(sock, self.key_file, self.cert_file)
+ self.sock = httplib.FakeSocket(sock, ssl)
+
+
+
+class Http(object):
+ """An HTTP client that handles:
+- all methods
+- caching
+- ETags
+- compression,
+- HTTPS
+- Basic
+- Digest
+- WSSE
+
+and more.
"""
- def __init__(self, cache=None):
+ 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."""
+ self.proxy_info = proxy_info
# Map domain name to an httplib connection
self.connections = {}
# The location of the cache, for now a directory
@@ -540,33 +769,52 @@
else:
self.cache = cache
- # tuples of name, password
- self.credentials = []
+ # Name/password
+ self.credentials = Credentials()
+
+ # Key/cert
+ self.certificates = KeyCerts()
# authorization objects
self.authorizations = []
+ # If set to False then no redirects are followed, even safe ones.
+ self.follow_redirects = True
+
+ # If 'follow_redirects' is True, and this is set to True then
+ # all redirecs are followed, including unsafe ones.
self.follow_all_redirects = False
+ self.ignore_etag = False
+
+ self.force_exception_to_status_code = False
+
+ self.timeout = timeout
+
def _auth_from_challenge(self, host, request_uri, headers, response,
content):
"""A generator that creates Authorization objects
that can be applied to requests.
"""
challenges = _parse_www_authenticate(response, 'www-authenticate')
- for cred in self.credentials:
+ for cred in self.credentials.iter(host):
for scheme in AUTH_SCHEME_ORDER:
if challenges.has_key(scheme):
- yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri,
headers, response, content, self)
+ yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri,
headers, response, content, self)
- def add_credentials(self, name, password):
+ def add_credentials(self, name, password, domain=""):
"""Add a name and password that will be used
any time a request requires authentication."""
- self.credentials.append((name, password))
+ self.credentials.add(name, password, domain)
+
+ def add_certificate(self, key, cert, domain):
+ """Add a key and cert that will be used
+ any time a request requires authentication."""
+ self.certificates.add(key, cert, domain)
def clear_credentials(self):
"""Remove all the names and passwords
that are used for authentication"""
- self.credentials = []
+ self.credentials.clear()
self.authorizations = []
def _conn_request(self, conn, request_uri, method, body, headers):
@@ -574,7 +822,10 @@
try:
conn.request(method, request_uri, body, headers)
response = conn.getresponse()
- except:
+ except socket.gaierror:
+ conn.close()
+ raise ServerNotFoundError("Unable to find the server at %s" %
conn.host)
+ except httplib.HTTPException, e:
if i == 0:
conn.close()
conn.connect()
@@ -584,7 +835,8 @@
else:
content = response.read()
response = Response(response)
- content = _decompressContent(response, content)
+ if method != "HEAD":
+ content = _decompressContent(response, content)
break;
return (response, content)
@@ -616,158 +868,219 @@
authorization.response(response, body)
break
- if (self.follow_all_redirects or method in ["GET", "HEAD"]) or
response.status == 303:
- if response.status in [300, 301, 302, 303, 307]:
+ if (self.follow_all_redirects or (method in ["GET", "HEAD"]) or
response.status == 303):
+ if self.follow_redirects and response.status in [300, 301, 302,
303, 307]:
# 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:
- raise RedirectMissingLocation( _("Redirected but the
response is missing a Location: header."))
+ 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'):
+ location = response['location']
+ (scheme, authority, path, query, fragment) =
parse_uri(location)
+ if authority == None:
+ response['location'] =
urlparse.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
_updateCache(headers, response, content, self.cache,
cachekey)
if headers.has_key('if-none-match'):
del headers['if-none-match']
if headers.has_key('if-modified-since'):
del headers['if-modified-since']
if response.has_key('location'):
- old_response = copy.deepcopy(response)
location = response['location']
- (scheme, authority, path, query, fragment) =
parse_uri(location)
- if authority == None:
- location = urlparse.urljoin(absolute_uri, 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
(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."))
+ raise RedirectLimit( _("Redirected more times than
rediection_limit allows."), response, content)
elif response.status in [200, 203] and method == "GET":
# 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
_updateCache(headers, response, content, self.cache, cachekey)
return (response, content)
- def request(self, uri, method="GET", body=None, headers=None,
redirections=DEFAULT_MAX_REDIRECTS):
- """Returns an httplib2.Response and the response content.
- uri - MUST be an absolute HTTP URI
+# Need to catch and rebrand some exceptions
+# Then need to optionally turn all exceptions into status codes
+# including all socket.* and httplib.* exceptions.
+
+
+ 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
+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.
+There is no restriction on the methods allowed.
+
+The 'body' is the entity body to be sent with the request. It is a string
+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
+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
+a string that contains the response entity body.
"""
- if headers is None:
- headers = {}
- else:
- headers = _normalize_headers(headers)
+ try:
+ if headers is None:
+ headers = {}
+ else:
+ headers = _normalize_headers(headers)
- if not headers.has_key('user-agent'):
- headers['user-agent'] = "Python-httplib2/%s" % __version__
+ if not headers.has_key('user-agent'):
+ headers['user-agent'] = "Python-httplib2/%s" % __version__
- (scheme, authority, path, query, fragment) = parse_uri(uri)
- authority = authority.lower()
- if not path:
- path = "/"
- # Could do syntax based normalization of the URI before
- # computing the digest. See Section 6.2.2 of Std 66.
- request_uri = query and "?".join([path, query]) or path
- defrag_uri = scheme + "://" + authority + request_uri
-
- if not self.connections.has_key(scheme+":"+authority):
- connection_type = (scheme == 'https') and httplib.HTTPSConnection
or httplib.HTTPConnection
- conn = self.connections[scheme+":"+authority] =
connection_type(authority)
- conn.set_debuglevel(debuglevel)
- else:
- conn = self.connections[scheme+":"+authority]
+ uri = iri2uri(uri)
- if method in ["GET", "HEAD"] and 'range' not in headers:
- headers['accept-encoding'] = 'compress, gzip'
+ (scheme, authority, request_uri, defrag_uri) = urlnorm(uri)
- info = rfc822.Message(StringIO.StringIO(""))
- cached_value = None
- if self.cache:
- cachekey = md5.new(defrag_uri).hexdigest()
- cached_value = self.cache.get(cachekey)
- if cached_value:
- #try:
- f = StringIO.StringIO(cached_value)
- info = rfc822.Message(f)
- content = cached_value.split('\r\n\r\n', 1)[1]
- #except:
- # self.cache.delete(cachekey)
- # cachekey = None
- else:
- cachekey = None
-
- if method in ["PUT"] and self.cache and info.has_key('etag'):
- # http://www.w3.org/1999/04/Editing/
- headers['if-match'] = info['etag']
-
- if method not in ["GET", "HEAD"] and self.cache and cachekey:
- # RFC 2616 Section 13.10
- self.cache.delete(cachekey)
-
- if method in ["GET", "HEAD"] and self.cache and 'range' not in headers:
- if info.has_key('-x-permanent-redirect-url'):
- # Should cached permanent redirects be counted in our
redirection count? For now, yes.
- (response, new_content) =
self.request(info['-x-permanent-redirect-url'], "GET", headers = headers,
redirections = redirections - 1)
- response.previous = Response(info)
- response.previous.fromcache = True
+ conn_key = scheme+":"+authority
+ if conn_key in self.connections:
+ conn = self.connections[conn_key]
else:
- # 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:
- # 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)
-
- if entry_disposition == "FRESH":
- if not cached_value:
- info['status'] = '504'
- content = ""
- response = Response(info)
- if cached_value:
- response.fromcache = True
- return (response, content)
-
- if entry_disposition == "STALE":
- if info.has_key('etag'):
- headers['if-none-match'] = info['etag']
- if info.has_key('last-modified'):
- headers['if-modified-since'] = info['last-modified']
- elif entry_disposition == "TRANSPARENT":
- pass
-
- (response, new_content) = self._request(conn, authority, uri,
request_uri, method, body, headers, redirections, cachekey)
-
- 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
- # and overwrite their values in info.
- # unless they are hop-by-hop, or are listed in the connection
header.
-
- for key in _get_end2end_headers(response):
- info[key] = response[key]
- merged_response = Response(info)
- if hasattr(response, "_stale_digest"):
- merged_response._stale_digest = response._stale_digest
- _updateCache(headers, merged_response, content, self.cache,
cachekey)
- response = merged_response
- response.status = 200
- response.fromcache = True
+ if not connection_type:
+ connection_type = (scheme == 'https') and
HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout
+ 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)
+ else:
+ 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:
+ headers['accept-encoding'] = 'compress, gzip'
- elif response.status == 200:
- content = new_content
+ 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)
+ try:
+ content = cached_value.split('\r\n\r\n', 1)[1]
+ except IndexError:
+ self.cache.delete(cachekey)
+ cachekey = None
+ cached_value = None
else:
+ cachekey = None
+
+ if method in ["PUT"] and self.cache and info.has_key('etag') and
not self.ignore_etag and 'if-match' not in headers:
+ # http://www.w3.org/1999/04/Editing/
+ headers['if-match'] = info['etag']
+
+ if method not in ["GET", "HEAD"] and self.cache and cachekey:
+ # RFC 2616 Section 13.10
self.cache.delete(cachekey)
- content = new_content
- else:
- (response, content) = self._request(conn, authority, uri,
request_uri, method, body, headers, redirections, cachekey)
+
+ if cached_value and method in ["GET", "HEAD"] and self.cache and
'range' not in headers:
+ if info.has_key('-x-permanent-redirect-url'):
+ # Should cached permanent redirects be counted in our
redirection count? For now, yes.
+ (response, new_content) =
self.request(info['-x-permanent-redirect-url'], "GET", headers = headers,
redirections = redirections - 1)
+ response.previous = Response(info)
+ response.previous.fromcache = True
+ else:
+ # 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:
+ # 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)
+
+ if entry_disposition == "FRESH":
+ if not cached_value:
+ info['status'] = '504'
+ content = ""
+ 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:
+ headers['if-none-match'] = info['etag']
+ if info.has_key('last-modified') and not
'last-modified' in headers:
+ headers['if-modified-since'] =
info['last-modified']
+ elif entry_disposition == "TRANSPARENT":
+ pass
+
+ (response, new_content) = self._request(conn, authority,
uri, request_uri, method, body, headers, redirections, cachekey)
+
+ 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
+ # and overwrite their values in info.
+ # unless they are hop-by-hop, or are listed in the
connection header.
+
+ for key in _get_end2end_headers(response):
+ info[key] = response[key]
+ merged_response = Response(info)
+ if hasattr(response, "_stale_digest"):
+ merged_response._stale_digest = response._stale_digest
+ _updateCache(headers, merged_response, content,
self.cache, cachekey)
+ response = merged_response
+ response.status = 200
+ response.fromcache = True
+
+ elif response.status == 200:
+ content = new_content
+ else:
+ self.cache.delete(cachekey)
+ content = new_content
+ else:
+ (response, content) = self._request(conn, authority, uri,
request_uri, method, body, headers, redirections, cachekey)
+ except Exception, 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)
+ elif isinstance(e, socket.timeout):
+ content = "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"
+ else:
+ raise
+
+
return (response, content)
class Response(dict):
- """An object more like rfc822.Message than httplib.HTTPResponse."""
+ """An object more like email.Message than httplib.HTTPResponse."""
"""Is this response from our local cache"""
fromcache = False
@@ -784,7 +1097,7 @@
previous = None
def __init__(self, info):
- # info is either an rfc822.Message or
+ # info is either an email.Message or
# an httplib.HTTPResponse object.
if isinstance(info, httplib.HTTPResponse):
for key, value in info.getheaders():
@@ -793,9 +1106,18 @@
self['status'] = str(self.status)
self.reason = info.reason
self.version = info.version
- elif isinstance(info, rfc822.Message):
+ elif isinstance(info, email.Message.Message):
for key, value in info.items():
self[key] = value
self.status = int(self['status'])
+ else:
+ for key, value in info.iteritems():
+ self[key] = value
+ self.status = int(self.get('status', self.status))
+ def __getattr__(self, name):
+ if name == 'dict':
+ return self
+ else:
+ raise AttributeError, name
diff -urN --exclude=CVS --exclude=.cvsignore --exclude=.svn
--exclude=.svnignore old/httplib2-0.2.0/httplib2/iri2uri.py
new/httplib2-0.4.0/httplib2/iri2uri.py
--- old/httplib2-0.2.0/httplib2/iri2uri.py 1970-01-01 01:00:00.000000000
+0100
+++ new/httplib2-0.4.0/httplib2/iri2uri.py 2007-09-04 06:02:06.000000000
+0200
@@ -0,0 +1,110 @@
+"""
+iri2uri
+
+Converts an IRI to a URI.
+
+"""
+__author__ = "Joe Gregorio (joe@xxxxxxxxxxxxxx)"
+__copyright__ = "Copyright 2006, Joe Gregorio"
+__contributors__ = []
+__version__ = "1.0.0"
+__license__ = "MIT"
+__history__ = """
+"""
+
+import urlparse
+
+
+# Convert an IRI to a URI following the rules in RFC 3987
+#
+# The characters we need to enocde and escape are defined in the spec:
+#
+# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
+# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
+# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
+# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
+# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
+# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
+# / %xD0000-DFFFD / %xE1000-EFFFD
+
+escape_range = [
+ (0xA0, 0xD7FF ),
+ (0xE000, 0xF8FF ),
+ (0xF900, 0xFDCF ),
+ (0xFDF0, 0xFFEF),
+ (0x10000, 0x1FFFD ),
+ (0x20000, 0x2FFFD ),
+ (0x30000, 0x3FFFD),
+ (0x40000, 0x4FFFD ),
+ (0x50000, 0x5FFFD ),
+ (0x60000, 0x6FFFD),
+ (0x70000, 0x7FFFD ),
+ (0x80000, 0x8FFFD ),
+ (0x90000, 0x9FFFD),
+ (0xA0000, 0xAFFFD ),
+ (0xB0000, 0xBFFFD ),
+ (0xC0000, 0xCFFFD),
+ (0xD0000, 0xDFFFD ),
+ (0xE1000, 0xEFFFD),
+ (0xF0000, 0xFFFFD ),
+ (0x100000, 0x10FFFD)
+]
+
+def encode(c):
+ retval = c
+ i = ord(c)
+ for low, high in escape_range:
+ if i < low:
+ break
+ if i >= low and i <= high:
+ retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')])
+ break
+ return retval
+
+
+def iri2uri(uri):
+ """Convert an IRI to a URI. Note that IRIs must be
+ passed in a unicode strings. That is, do not utf-8 encode
+ the IRI before passing it into the function."""
+ if isinstance(uri ,unicode):
+ (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
+ authority = authority.encode('idna')
+ # For each character in 'ucschar' or 'iprivate'
+ # 1. encode as utf-8
+ # 2. then %-encode each octet of that utf-8
+ uri = urlparse.urlunsplit((scheme, authority, path, query, fragment))
+ uri = "".join([encode(c) for c in uri])
+ return uri
+
+if __name__ == "__main__":
+ import unittest
+
+ class Test(unittest.TestCase):
+
+ def test_uris(self):
+ """Test that URIs are invariant under the transformation."""
+ invariant = [
+ u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
+ u"http://www.ietf.org/rfc/rfc2396.txt",
+ u"ldap://[2001:db8::7]/c=GB?objectClass?one",
+ u"mailto:John.Doe@xxxxxxxxxxx",
+ u"news:comp.infosystems.www.servers.unix",
+ u"tel:+1-816-555-1212",
+ u"telnet://192.0.2.16:80/",
+ u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ]
+ for uri in invariant:
+ self.assertEqual(uri, iri2uri(uri))
+
+ def test_iri(self):
+ """ Test that the right type of escaping is done for each part of
the URI."""
+ self.assertEqual("http://xn--o3h.com/%E2%98%84",
iri2uri(u"http://\N{COMET}.com/\N{COMET}"))
+ self.assertEqual("http://bitworking.org/?fred=%E2%98%84",
iri2uri(u"http://bitworking.org/?fred=\N{COMET}"))
+ self.assertEqual("http://bitworking.org/#%E2%98%84",
iri2uri(u"http://bitworking.org/#\N{COMET}"))
+ self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}"))
+ self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84",
iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))
+ self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84",
iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")))
+ self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84",
iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8')))
+
+ unittest.main()
+
+
diff -urN --exclude=CVS --exclude=.cvsignore --exclude=.svn
--exclude=.svnignore old/httplib2-0.2.0/PKG-INFO new/httplib2-0.4.0/PKG-INFO
--- old/httplib2-0.2.0/PKG-INFO 2006-07-01 07:58:28.000000000 +0200
+++ new/httplib2-0.4.0/PKG-INFO 2007-10-23 17:27:36.000000000 +0200
@@ -1,12 +1,12 @@
Metadata-Version: 1.0
Name: httplib2
-Version: 0.2.0
+Version: 0.4.0
Summary: A comprehensive HTTP client library.
Home-page: http://bitworking.org/projects/httplib2/
Author: Joe Gregorio
Author-email: joe@xxxxxxxxxxxxxx
License: MIT
-Download-URL:
http://bitworking.org/projects/httplib2/dist/httplib2-0.2.0.tar.gz
+Download-URL:
http://bitworking.org/projects/httplib2/dist/httplib2-0.4.0.tar.gz
Description:
A comprehensive HTTP client library, ``httplib2`` supports many
features left out of other HTTP libraries.
@@ -52,7 +52,7 @@
A large and growing set of unit tests.
Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
+Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
diff -urN --exclude=CVS --exclude=.cvsignore --exclude=.svn
--exclude=.svnignore old/httplib2-0.2.0/setup.py new/httplib2-0.4.0/setup.py
--- old/httplib2-0.2.0/setup.py 2006-06-29 07:28:23.000000000 +0200
+++ new/httplib2-0.4.0/setup.py 2007-10-23 17:25:46.000000000 +0200
@@ -1,5 +1,5 @@
from distutils.core import setup
-VERSION = '0.2.0'
+VERSION = '0.4.0'
setup(name='httplib2',
version=VERSION,
author='Joe Gregorio',
@@ -54,7 +54,7 @@
""",
packages=['httplib2'],
classifiers=[
- 'Development Status :: 3 - Alpha',
+ 'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Remember to have fun...
---------------------------------------------------------------------
To unsubscribe, e-mail: opensuse-commit+unsubscribe@xxxxxxxxxxxx
For additional commands, e-mail: opensuse-commit+help@xxxxxxxxxxxx
| < Previous | Next > |