Repository: google/google-api-python-client
Pull request #406 (2017-06-14 18:44:35)

MediaIoBaseDownload: Handle unknown media length
Some media download APIs does not support partial downloads and do not return a 'Content-length' or 'Content-range' header and is therefore never considered done.

This change is to consider responses where the media length is unknown to be done.

This commit could potentially break media downloads where the API supports partial download and only returns a 'Content-length' header when the entire file has been served/read.

This commit fixes issue #15


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 67a850990..ba72d1515 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,26 @@
+v1.6.4
+  Version 1.6.4
+
+  Bugfix release
+
+  - Warn when google-auth credentials are used but google-auth-httplib2 isn't available. (#443)
+
+v1.6.3
+  Version 1.6.3
+
+  Bugfix release
+
+  - Add notification of maintenance mode to README. (#410)
+  - Fix generation of methods with abnormal page token conventions. (#338)
+  - Raise ValueError is credentials and developerKey are both specified. (#358)
+  - Re-generate documentation. (#364, #373, #401)
+  - Fix method signature documentation for multiline required parameters. (#374)
+  - Fix ZeroDivisionError in MediaDownloadProgress.progress. (#377)
+  - Fix dead link to WebTest in README. (#378)
+  - Fix details missing in googleapiclient.errors.HttpError. (#412)
+  - Don't treat httplib2.Credentials as oauth credentials. (#425)
+  - Various fixes to the Django sample. (#413)
+
 v1.6.2
   Version 1.6.2
 
diff --git a/README.md b/README.md
index f7f67727b..6eda8cfb7 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+# Library maintenance
+This client library is supported but in maintenance mode only.  We are fixing necessary bugs and adding essential features to ensure this library continues to meet your needs for accessing Google APIs.  Non-critical issues will be closed.  Any issue may be reopened if it is causing ongoing problems.
+
 # About
 This is the Python client library for Google's discovery based APIs. To get started, please see the [full documentation for this library](http://google.github.io/google-api-python-client). Additionally, [dynamically generated documentation](http://api-python-client-doc.appspot.com/) is available for all of the APIs supported by this library.
 
diff --git a/docs/epy/googleapiclient.http-pysrc.html b/docs/epy/googleapiclient.http-pysrc.html
index efaa79981..3506be86b 100644
--- a/docs/epy/googleapiclient.http-pysrc.html
+++ b/docs/epy/googleapiclient.http-pysrc.html
@@ -844,7 +844,7 @@ googleapiclient.http.MediaUpload.chunksize" class="py-name" href="#" onclick="re
 <a name="L655"></a><tt class="py-lineno"> 655</tt>  <tt class="py-line"><tt class="py-docstring">    Returns:</tt> </tt>
 <a name="L656"></a><tt class="py-lineno"> 656</tt>  <tt class="py-line"><tt class="py-docstring">      (status, done): (MediaDownloadStatus, boolean)</tt> </tt>
 <a name="L657"></a><tt class="py-lineno"> 657</tt>  <tt class="py-line"><tt class="py-docstring">         The value of 'done' will be True when the media has been fully</tt> </tt>
-<a name="L658"></a><tt class="py-lineno"> 658</tt>  <tt class="py-line"><tt class="py-docstring">         downloaded.</tt> </tt>
+<a name="L658"></a><tt class="py-lineno"> 658</tt>  <tt class="py-line"><tt class="py-docstring">         downloaded or the total size of the media is unknown.</tt> </tt>
 <a name="L659"></a><tt class="py-lineno"> 659</tt>  <tt class="py-line"><tt class="py-docstring"></tt> </tt>
 <a name="L660"></a><tt class="py-lineno"> 660</tt>  <tt class="py-line"><tt class="py-docstring">    Raises:</tt> </tt>
 <a name="L661"></a><tt class="py-lineno"> 661</tt>  <tt class="py-line"><tt class="py-docstring">      googleapiclient.errors.HttpError if the response was not a 2xx.</tt> </tt>
@@ -873,7 +873,7 @@ googleapiclient.http.MediaUpload.chunksize" class="py-name" href="#" onclick="re
 <a name="L684"></a><tt class="py-lineno"> 684</tt>  <tt class="py-line">      <tt class="py-keyword">elif</tt> <tt class="py-string">'content-length'</tt> <tt class="py-keyword">in</tt> <tt class="py-name">resp</tt><tt class="py-op">:</tt> </tt>
 <a name="L685"></a><tt class="py-lineno"> 685</tt>  <tt class="py-line">        <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_total_size</tt> <tt class="py-op">=</tt> <tt class="py-name">int</tt><tt class="py-op">(</tt><tt class="py-name">resp</tt><tt class="py-op">[</tt><tt class="py-string">'content-length'</tt><tt class="py-op">]</tt><tt class="py-op">)</tt> </tt>
 <a name="L686"></a><tt class="py-lineno"> 686</tt>  <tt class="py-line"> </tt>
-<a name="L687"></a><tt class="py-lineno"> 687</tt>  <tt class="py-line">      <tt class="py-keyword">if</tt> <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_progress</tt> <tt class="py-op">==</tt> <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_total_size</tt><tt class="py-op">:</tt> </tt>
+<a name="L687"></a><tt class="py-lineno"> 687</tt>  <tt class="py-line">      <tt class="py-keyword">if</tt> <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_total_size</tt> <tt class="py-keyword">is</tt> <tt class="py-name">None</tt> <tt class="py-keyword">or</tt> <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_progress</tt> <tt class="py-op">==</tt> <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_total_size</tt><tt class="py-op">:</tt> </tt>
 <a name="L688"></a><tt class="py-lineno"> 688</tt>  <tt class="py-line">        <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_done</tt> <tt class="py-op">=</tt> <tt class="py-name">True</tt> </tt>
 <a name="L689"></a><tt class="py-lineno"> 689</tt>  <tt class="py-line">      <tt class="py-keyword">return</tt> <tt id="link-86" class="py-name" targets="Class googleapiclient.http.MediaDownloadProgress=googleapiclient.http.MediaDownloadProgress-class.html"><a title="googleapiclient.http.MediaDownloadProgress" class="py-name" href="#" onclick="return doclink('link-86', 'MediaDownloadProgress', 'link-86');">MediaDownloadProgress</a></tt><tt class="py-op">(</tt><tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_progress</tt><tt class="py-op">,</tt> <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_total_size</tt><tt class="py-op">)</tt><tt class="py-op">,</tt> <tt class="py-name">self</tt><tt class="py-op">.</tt><tt class="py-name">_done</tt> </tt>
 <a name="L690"></a><tt class="py-lineno"> 690</tt>  <tt class="py-line">    <tt class="py-keyword">else</tt><tt class="py-op">:</tt> </tt>
@@ -2189,7 +2189,7 @@ expandto(location.href);
 <table border="0" cellpadding="0" cellspacing="0" width="100%%">
   <tr>
     <td align="left" class="footer">
-    Generated by Epydoc 3.0.1 on Mon Jun  5 13:38:40 2017
+    Generated by Epydoc 3.0.1 on Wed Jun 14 20:18:21 2017
     </td>
     <td align="right" class="footer">
       <a target="mainFrame" href="http://epydoc.sourceforge.net"
diff --git a/docs/epy/googleapiclient.http.MediaIoBaseDownload-class.html b/docs/epy/googleapiclient.http.MediaIoBaseDownload-class.html
index 06f378178..3bf1a5752 100644
--- a/docs/epy/googleapiclient.http.MediaIoBaseDownload-class.html
+++ b/docs/epy/googleapiclient.http.MediaIoBaseDownload-class.html
@@ -266,7 +266,7 @@ Args:
 Returns:
   (status, done): (MediaDownloadStatus, boolean)
      The value of 'done' will be True when the media has been fully
-     downloaded.
+     downloaded or the total size of the media is unknown.
 
 Raises:
   googleapiclient.errors.HttpError if the response was not a 2xx.
@@ -308,7 +308,7 @@ Raises:
 <table border="0" cellpadding="0" cellspacing="0" width="100%%">
   <tr>
     <td align="left" class="footer">
-    Generated by Epydoc 3.0.1 on Mon Jun  5 13:38:40 2017
+    Generated by Epydoc 3.0.1 on Wed Jun 14 20:18:20 2017
     </td>
     <td align="right" class="footer">
       <a target="mainFrame" href="http://epydoc.sourceforge.net"
diff --git a/googleapiclient/__init__.py b/googleapiclient/__init__.py
index 4af0c3c25..184219af3 100644
--- a/googleapiclient/__init__.py
+++ b/googleapiclient/__init__.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = "1.6.2"
+__version__ = "1.6.4"
 
 # Set default logging handler to avoid "No handler found" warnings.
 import logging
diff --git a/googleapiclient/_auth.py b/googleapiclient/_auth.py
index 87d370951..ada4ffb15 100644
--- a/googleapiclient/_auth.py
+++ b/googleapiclient/_auth.py
@@ -14,14 +14,20 @@
 
 """Helpers for authentication using oauth2client or google-auth."""
 
+import httplib2
+
 try:
     import google.auth
     import google.auth.credentials
-    import google_auth_httplib2
     HAS_GOOGLE_AUTH = True
 except ImportError:  # pragma: NO COVER
     HAS_GOOGLE_AUTH = False
 
+try:
+    import google_auth_httplib2
+except ImportError:  # pragma: NO COVER
+    google_auth_httplib2 = None
+
 try:
     import oauth2client
     import oauth2client.client
@@ -29,8 +35,6 @@ try:
 except ImportError:  # pragma: NO COVER
     HAS_OAUTH2CLIENT = False
 
-from googleapiclient.http import build_http
-
 
 def default_credentials():
     """Returns Application Default Credentials."""
@@ -84,9 +88,56 @@ def authorized_http(credentials):
         Union[httplib2.Http, google_auth_httplib2.AuthorizedHttp]: An
             authorized http client.
     """
+    from googleapiclient.http import build_http
+
     if HAS_GOOGLE_AUTH and isinstance(
             credentials, google.auth.credentials.Credentials):
+        if google_auth_httplib2 is None:
+            raise ValueError(
+                'Credentials from google.auth specified, but '
+                'google-api-python-client is unable to use these credentials '
+                'unless google-auth-httplib2 is installed. Please install '
+                'google-auth-httplib2.')
         return google_auth_httplib2.AuthorizedHttp(credentials,
                                                    http=build_http())
     else:
         return credentials.authorize(build_http())
+
+
+def refresh_credentials(credentials):
+    # Refresh must use a new http instance, as the one associated with the
+    # credentials could be a AuthorizedHttp or an oauth2client-decorated
+    # Http instance which would cause a weird recursive loop of refreshing
+    # and likely tear a hole in spacetime.
+    refresh_http = httplib2.Http()
+    if HAS_GOOGLE_AUTH and isinstance(
+            credentials, google.auth.credentials.Credentials):
+        request = google_auth_httplib2.Request(refresh_http)
+        return credentials.refresh(request)
+    else:
+        return credentials.refresh(refresh_http)
+
+
+def apply_credentials(credentials, headers):
+    # oauth2client and google-auth have the same interface for this.
+    return credentials.apply(headers)
+
+
+def is_valid(credentials):
+    if HAS_GOOGLE_AUTH and isinstance(
+            credentials, google.auth.credentials.Credentials):
+        return credentials.valid
+    else:
+        return not credentials.access_token_expired
+
+
+def get_credentials_from_http(http):
+    if http is None:
+        return None
+    elif hasattr(http.request, 'credentials'):
+        return http.request.credentials
+    elif (hasattr(http, 'credentials')
+          and not isinstance(http.credentials, httplib2.Credentials)):
+        return http.credentials
+    else:
+        return None
diff --git a/googleapiclient/errors.py b/googleapiclient/errors.py
index 6a741f86d..bab14189f 100644
--- a/googleapiclient/errors.py
+++ b/googleapiclient/errors.py
@@ -46,6 +46,7 @@ class HttpError(Error):
         raise TypeError("HTTP content should be bytes")
     self.content = content
     self.uri = uri
+    self.error_details = ''
 
   def _get_reason(self):
     """Calculate the reason for the error from the response content."""
@@ -54,9 +55,13 @@ class HttpError(Error):
       data = json.loads(self.content.decode('utf-8'))
       if isinstance(data, dict):
         reason = data['error']['message']
+        if 'details' in data['error']:
+            self.error_details = data['error']['details']
       elif isinstance(data, list) and len(data) > 0:
         first_error = data[0]
         reason = first_error['error']['message']
+        if 'details' in first_error['error']:
+            self.error_details = first_error['error']['details']
     except (ValueError, KeyError, TypeError):
       pass
     if reason is None:
@@ -64,7 +69,11 @@ class HttpError(Error):
     return reason
 
   def __repr__(self):
-    if self.uri:
+    reason = self._get_reason()
+    if self.error_details:
+      return '<HttpError %s when requesting %s returned "%s". Details: "%s">' % \
+             (self.resp.status, self.uri, reason.strip(), self.error_details)
+    elif self.uri:
       return '<HttpError %s when requesting %s returned "%s">' % (
           self.resp.status, self.uri, self._get_reason().strip())
     else:
diff --git a/googleapiclient/http.py b/googleapiclient/http.py
index 302e0e70e..ee8d6e3c3 100644
--- a/googleapiclient/http.py
+++ b/googleapiclient/http.py
@@ -62,6 +62,7 @@ try:
 except ImportError:
   from oauth2client import _helpers as util
 
+from googleapiclient import _auth
 from googleapiclient import mimeparse
 from googleapiclient.errors import BatchError
 from googleapiclient.errors import HttpError
@@ -88,7 +89,7 @@ def _should_retry_response(resp_status, content):
 
   Args:
     resp_status: The response status received.
-    content: The response content body. 
+    content: The response content body.
 
   Returns:
     True if the response should be retried, otherwise False.
@@ -111,7 +112,10 @@ def _should_retry_response(resp_status, content):
     # Content is in JSON format.
     try:
       data = json.loads(content.decode('utf-8'))
-      reason = data['error']['errors'][0]['reason']
+      if isinstance(data, dict):
+        reason = data['error']['errors'][0]['reason']
+      else:
+        reason = data[0]['error']['errors']['reason']
     except (UnicodeDecodeError, ValueError, KeyError):
       LOGGER.warning('Invalid JSON content from response: %s', content)
       return False
@@ -509,7 +513,6 @@ class MediaFileUpload(MediaIoBaseUpload):
   Construct a MediaFileUpload and pass as the media_body parameter of the
   method. For example, if we had a service that allowed uploading images:
 
-
     media = MediaFileUpload('cow.png', mimetype='image/png',
       chunksize=1024*1024, resumable=True)
     farm.animals().insert(
@@ -653,9 +656,9 @@ class MediaIoBaseDownload(object):
             request only once.
 
     Returns:
-      (status, done): (MediaDownloadStatus, boolean)
+      (status, done): (MediaDownloadProgress, boolean)
          The value of 'done' will be True when the media has been fully
-         downloaded.
+         downloaded or the total size of the media is unknown.
 
     Raises:
       googleapiclient.errors.HttpError if the response was not a 2xx.
@@ -684,7 +687,7 @@ class MediaIoBaseDownload(object):
       elif 'content-length' in resp:
         self._total_size = int(resp['content-length'])
 
-      if self._progress == self._total_size:
+      if self._total_size is None or self._progress == self._total_size:
         self._done = True
       return MediaDownloadProgress(self._progress, self._total_size), self._done
     else:
@@ -1126,21 +1129,25 @@ class BatchHttpRequest(object):
     # If there is no http per the request then refresh the http passed in
     # via execute()
     creds = None
-    if request.http is not None and hasattr(request.http.request,
-        'credentials'):
-      creds = request.http.request.credentials
-    elif http is not None and hasattr(http.request, 'credentials'):
-      creds = http.request.credentials
+    request_credentials = False
+
+    if request.http is not None:
+      creds = _auth.get_credentials_from_http(request.http)
+      request_credentials = True
+
+    if creds is None and http is not None:
+      creds = _auth.get_credentials_from_http(http)
+
     if creds is not None:
       if id(creds) not in self._refreshed_credentials:
-        creds.refresh(http)
+        _auth.refresh_credentials(creds)
         self._refreshed_credentials[id(creds)] = 1
 
     # Only apply the credentials if we are using the http object passed in,
     # otherwise apply() will get called during _serialize_request().
-    if request.http is None or not hasattr(request.http.request,
-        'credentials'):
-      creds.apply(request.headers)
+    if request.http is None or not request_credentials:
+      _auth.apply_credentials(creds, request.headers)
+
 
   def _id_to_header(self, id_):
     """Convert an id to a Content-ID header value.
@@ -1200,9 +1207,10 @@ class BatchHttpRequest(object):
     msg = MIMENonMultipart(major, minor)
     headers = request.headers.copy()
 
-    if request.http is not None and hasattr(request.http.request,
-        'credentials'):
-      request.http.request.credentials.apply(headers)
+    if request.http is not None:
+      credentials = _auth.get_credentials_from_http(request.http)
+      if credentials is not None:
+        _auth.apply_credentials(credentials, headers)
 
     # MIMENonMultipart adds its own Content-Type header.
     if 'content-type' in headers:
@@ -1409,11 +1417,11 @@ class BatchHttpRequest(object):
 
     # Special case for OAuth2Credentials-style objects which have not yet been
     # refreshed with an initial access_token.
-    if getattr(http.request, 'credentials', None) is not None:
-      creds = http.request.credentials
-      if not getattr(creds, 'access_token', None):
+    creds = _auth.get_credentials_from_http(http)
+    if creds is not None:
+      if not _auth.is_valid(creds):
         LOGGER.info('Attempting refresh to obtain initial access_token')
-        creds.refresh(http)
+        _auth.refresh_credentials(creds)
 
     self._execute(http, self._order, self._requests)
 
diff --git a/samples/django_sample/Makefile b/samples/django_sample/Makefile
new file mode 100644
index 000000000..35d29f048
--- /dev/null
+++ b/samples/django_sample/Makefile
@@ -0,0 +1,21 @@
+pip: 
+	@pip install -r requirements.txt
+
+
+syncdb:
+	@python manage.py syncdb
+
+
+run: 
+	@python manage.py runserver 0.0.0.0:8000
+
+
+setup: pip syncdb run
+
+
+shell: 
+	@python manage.py shell
+
+
+test:
+	@python manage.py test
diff --git a/samples/django_sample/manage.py b/samples/django_sample/manage.py
index b2f07f15f..4146caaab 100755
--- a/samples/django_sample/manage.py
+++ b/samples/django_sample/manage.py
@@ -2,7 +2,7 @@
 from __future__ import absolute_import
 from django.core.management import execute_manager
 try:
-    from . import settings # Assumed to be in the same directory.
+    import settings # Assumed to be in the same directory.
 except ImportError:
     import sys
     sys.stderr.write("""Error: Can't find the file 'settings.py' in the
diff --git a/samples/django_sample/plus/models.py b/samples/django_sample/plus/models.py
index 6b9f762f8..46a0225ae 100644
--- a/samples/django_sample/plus/models.py
+++ b/samples/django_sample/plus/models.py
@@ -1,12 +1,8 @@
-import pickle
-import base64
-
 from django.contrib import admin
 from django.contrib.auth.models import User
 from django.db import models
 
-from oauth2client.contrib.django_orm import FlowField
-from oauth2client.contrib.django_orm import CredentialsField
+from oauth2client.contrib.django_util.models import CredentialsField
 
 
 class CredentialsModel(models.Model):
@@ -16,6 +12,3 @@ class CredentialsModel(models.Model):
 
 class CredentialsAdmin(admin.ModelAdmin):
     pass
-
-
-admin.site.register(CredentialsModel, CredentialsAdmin)
diff --git a/samples/django_sample/plus/views.py b/samples/django_sample/plus/views.py
index 979c2b013..bc454619a 100644
--- a/samples/django_sample/plus/views.py
+++ b/samples/django_sample/plus/views.py
@@ -4,8 +4,6 @@ import httplib2
 
 from googleapiclient.discovery import build
 from django.contrib.auth.decorators import login_required
-from django.core.urlresolvers import reverse
-from django.http import HttpResponse
 from django.http import HttpResponseBadRequest
 from django.http import HttpResponseRedirect
 from django.shortcuts import render
@@ -13,23 +11,22 @@ from django_sample.plus.models import CredentialsModel
 from django_sample import settings
 from oauth2client.contrib import xsrfutil
 from oauth2client.client import flow_from_clientsecrets
-from oauth2client.contrib.django_orm import Storage
+from oauth2client.contrib.django_util.storage import DjangoORMStorage
 
 # CLIENT_SECRETS, name of a file containing the OAuth 2.0 information for this
 # application, including client_id and client_secret, which are found
 # on the API Access tab on the Google APIs
 # Console <http://code.google.com/apis/console>
-CLIENT_SECRETS = os.path.join(os.path.dirname(__file__), '..', 'client_secrets.json')
 
 FLOW = flow_from_clientsecrets(
-    CLIENT_SECRETS,
+    settings.GOOGLE_OAUTH2_CLIENT_SECRETS_JSON,
     scope='https://www.googleapis.com/auth/plus.me',
     redirect_uri='http://localhost:8000/oauth2callback')
 
 
 @login_required
 def index(request):
-  storage = Storage(CredentialsModel, 'id', request.user, 'credential')
+  storage = DjangoORMStorage(CredentialsModel, 'id', request.user, 'credential')
   credential = storage.get()
   if credential is None or credential.invalid == True:
     FLOW.params['state'] = xsrfutil.generate_token(settings.SECRET_KEY,
@@ -56,6 +53,6 @@ def auth_return(request):
                                  request.user):
     return  HttpResponseBadRequest()
   credential = FLOW.step2_exchange(request.REQUEST)
-  storage = Storage(CredentialsModel, 'id', request.user, 'credential')
+  storage = DjangoORMStorage(CredentialsModel, 'id', request.user, 'credential')
   storage.put(credential)
   return HttpResponseRedirect("/")
diff --git a/samples/django_sample/requirements.txt b/samples/django_sample/requirements.txt
new file mode 100644
index 000000000..01ff08324
--- /dev/null
+++ b/samples/django_sample/requirements.txt
@@ -0,0 +1,11 @@
+Django==1.3
+google-api-python-client==1.6.2
+httplib2==0.10.3
+jsonpickle==0.9.4
+oauth2client==4.1.2
+pyasn1==0.2.3
+pyasn1-modules==0.0.9
+pytz==2017.2
+rsa==3.4.2
+six==1.10.0
+uritemplate==3.0.0
diff --git a/samples/django_sample/settings.py b/samples/django_sample/settings.py
index ceb4b2922..76a16a8d3 100644
--- a/samples/django_sample/settings.py
+++ b/samples/django_sample/settings.py
@@ -79,5 +79,7 @@ INSTALLED_APPS = (
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.sites',
-    'django_sample.plus'
+    'plus'
 )
+
+GOOGLE_OAUTH2_CLIENT_SECRETS_JSON = 'client_secrets.json'
diff --git a/tox.ini b/tox.ini
index 2664d2eb1..444213ecc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,10 +13,11 @@ deps =
        mox
        pyopenssl
        pycrypto==2.6
-       django
+       django<2.0.0; python_version < '3.0.0'
+       django>=2.0.0; python_version > '3.0.0'
        webtest
        nose
        coverage>=3.6,<3.99
        unittest2
        mock
-commands = nosetests --with-coverage --cover-package=googleapiclient --nocapture --cover-erase --cover-tests --cover-branches --cover-min-percentage=85
+commands = nosetests --with-coverage --cover-package=googleapiclient --nocapture --cover-erase --cover-tests --cover-branches --cover-min-percentage=85 []


Test patch:
diff --git a/tests/test__auth.py b/tests/test__auth.py
index 68e7aae6b..003098e65 100644
--- a/tests/test__auth.py
+++ b/tests/test__auth.py
@@ -68,7 +68,9 @@ class TestAuthWithGoogleAuth(unittest2.TestCase):
 
         authorized_http = _auth.authorized_http(credentials)
 
-        self.assertIsInstance(authorized_http, google_auth_httplib2.AuthorizedHttp)
+        self.assertIsInstance(
+            authorized_http,
+            google_auth_httplib2.AuthorizedHttp)
         self.assertEqual(authorized_http.credentials, credentials)
         self.assertIsInstance(authorized_http.http, httplib2.Http)
         self.assertIsInstance(authorized_http.http.timeout, int)
@@ -138,3 +140,16 @@ class TestAuthWithoutAuth(unittest2.TestCase):
     def test_default_credentials(self):
         with self.assertRaises(EnvironmentError):
             print(_auth.default_credentials())
+
+
+class TestGoogleAuthWithoutHttplib2(unittest2.TestCase):
+    def setUp(self):
+        _auth.google_auth_httplib2 = None
+
+    def tearDown(self):
+        _auth.google_auth_httplib2 = google_auth_httplib2
+
+    def test_default_credentials(self):
+        credentials = mock.Mock(spec=google.auth.credentials.Credentials)
+        with self.assertRaises(ValueError):
+            _auth.authorized_http(credentials)
diff --git a/tests/test_errors.py b/tests/test_errors.py
index 8a580302c..e4d2f0948 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -41,7 +41,8 @@ JSON_ERROR_CONTENT = b"""
    }
   ],
   "code": 400,
-  "message": "country is required"
+  "message": "country is required",
+  "details": "error details"
  }
 }
 """
@@ -61,7 +62,7 @@ class Error(unittest.TestCase):
         {'status':'400', 'content-type': 'application/json'},
         reason='Failed')
     error = HttpError(resp, content, uri='http://example.org')
-    self.assertEqual(str(error), '<HttpError 400 when requesting http://example.org returned "country is required">')
+    self.assertEqual(str(error), '<HttpError 400 when requesting http://example.org returned "country is required". Details: "error details">')
 
   def test_bad_json_body(self):
     """Test handling of bodies with invalid json."""
diff --git a/tests/test_http.py b/tests/test_http.py
index fe7467263..9d482663f 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -62,12 +62,17 @@ from oauth2client.client import Credentials
 
 class MockCredentials(Credentials):
   """Mock class for all Credentials objects."""
-  def __init__(self, bearer_token):
+  def __init__(self, bearer_token, expired=False):
     super(MockCredentials, self).__init__()
     self._authorized = 0
     self._refreshed = 0
     self._applied = 0
     self._bearer_token = bearer_token
+    self._access_token_expired = expired
+
+  @property
+  def access_token_expired(self):
+    return self._access_token_expired
 
   def authorize(self, http):
     self._authorized += 1
@@ -123,7 +128,6 @@ class HttpMockWithErrors(object):
           ex = TimeoutError()
         else:
           ex = socket.error()
-        
         if self.num_errors == 2:
           #first try a broken pipe error (#218)
           ex.errno = socket.errno.EPIPE
@@ -572,6 +576,29 @@ class TestMediaIoBaseDownload(unittest.TestCase):
     self.assertEqual(0, download._total_size)
     self.assertEqual(0, status.progress())
 
+  def test_media_io_base_download_unknown_media_size(self):
+    self.request.http = HttpMockSequence([
+      ({'status': '200'}, b'123')
+    ])
+
+    download = MediaIoBaseDownload(
+      fd=self.fd, request=self.request, chunksize=3)
+
+    self.assertEqual(self.fd, download._fd)
+    self.assertEqual(0, download._progress)
+    self.assertEqual(None, download._total_size)
+    self.assertEqual(False, download._done)
+    self.assertEqual(self.request.uri, download._uri)
+
+    status, done = download.next_chunk()
+
+    self.assertEqual(self.fd.getvalue(), b'123')
+    self.assertEqual(True, done)
+    self.assertEqual(3, download._progress)
+    self.assertEqual(None, download._total_size)
+    self.assertEqual(0, status.progress())
+
+
 EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
 Content-Type: application/json
 MIME-Version: 1.0
@@ -734,6 +761,20 @@ NOT_CONFIGURED_RESPONSE = """{
  }
 }"""
 
+LIST_NOT_CONFIGURED_RESPONSE = """[
+ "error": {
+  "errors": [
+   {
+    "domain": "usageLimits",
+    "reason": "accessNotConfigured",
+    "message": "Access Not Configured"
+   }
+  ],
+  "code": 403,
+  "message": "Access Not Configured"
+ }
+]"""
+
 class Callbacks(object):
   def __init__(self):
     self.responses = {}
@@ -951,6 +992,29 @@ class TestHttpRequest(unittest.TestCase):
       request.execute()
     request._sleep.assert_not_called()
 
+  def test_no_retry_403_list_fails(self):
+    http = HttpMockSequence([
+        ({'status': '403'}, LIST_NOT_CONFIGURED_RESPONSE),
+        ({'status': '200'}, '{}')
+        ])
+    model = JsonModel()
+    uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
+    method = u'POST'
+    request = HttpRequest(
+        http,
+        model.response,
+        uri,
+        method=method,
+        body=u'{}',
+        headers={'content-type': 'application/json'})
+
+    request._rand = lambda: 1.0
+    request._sleep =  mock.MagicMock()
+
+    with self.assertRaises(HttpError):
+      request.execute()
+    request._sleep.assert_not_called()
+
 class TestBatch(unittest.TestCase):
 
   def setUp(self):
@@ -1128,10 +1192,7 @@ class TestBatch(unittest.TestCase):
   def test_execute_initial_refresh_oauth2(self):
     batch = BatchHttpRequest()
     callbacks = Callbacks()
-    cred = MockCredentials('Foo')
-
-    # Pretend this is a OAuth2Credentials object
-    cred.access_token = None
+    cred = MockCredentials('Foo', expired=True)
 
     http = HttpMockSequence([
       ({'status': '200',