Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#334: vendor-in apitools subset #400

Merged
merged 6 commits into from
Dec 2, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions _gcloud_vendor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Dependencies "vendored in", due to dependencies, Python versions, etc.

Current set
-----------

``apitools`` (pending release to PyPI, plus acceptable Python version
support for its dependencies). Review before M2.
"""
1 change: 1 addition & 0 deletions _gcloud_vendor/apitools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package stub."""
1 change: 1 addition & 0 deletions _gcloud_vendor/apitools/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package stub."""
1 change: 1 addition & 0 deletions _gcloud_vendor/apitools/base/py/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package stub."""
100 changes: 100 additions & 0 deletions _gcloud_vendor/apitools/base/py/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python
"""Exceptions for generated client libraries."""


class Error(Exception):
"""Base class for all exceptions."""


class TypecheckError(Error, TypeError):
"""An object of an incorrect type is provided."""


class NotFoundError(Error):
"""A specified resource could not be found."""


class UserError(Error):
"""Base class for errors related to user input."""


class InvalidDataError(Error):
"""Base class for any invalid data error."""


class CommunicationError(Error):
"""Any communication error talking to an API server."""


class HttpError(CommunicationError):
"""Error making a request. Soon to be HttpError."""

def __init__(self, response, content, url):
super(HttpError, self).__init__()
self.response = response
self.content = content
self.url = url

def __str__(self):
content = self.content.decode('ascii', 'replace')
return 'HttpError accessing <%s>: response: <%s>, content <%s>' % (
self.url, self.response, content)

@property
def status_code(self):
# TODO(craigcitro): Turn this into something better than a
# KeyError if there is no status.
return int(self.response['status'])

@classmethod
def FromResponse(cls, http_response):
return cls(http_response.info, http_response.content,
http_response.request_url)


class InvalidUserInputError(InvalidDataError):
"""User-provided input is invalid."""


class InvalidDataFromServerError(InvalidDataError, CommunicationError):
"""Data received from the server is malformed."""


class BatchError(Error):
"""Error generated while constructing a batch request."""


class ConfigurationError(Error):
"""Base class for configuration errors."""


class GeneratedClientError(Error):
"""The generated client configuration is invalid."""


class ConfigurationValueError(UserError):
"""Some part of the user-specified client configuration is invalid."""


class ResourceUnavailableError(Error):
"""User requested an unavailable resource."""


class CredentialsError(Error):
"""Errors related to invalid credentials."""


class TransferError(CommunicationError):
"""Errors related to transfers."""


class TransferInvalidError(TransferError):
"""The given transfer is invalid."""


class NotYetImplementedError(GeneratedClientError):
"""This functionality is not yet implemented."""


class StreamExhausted(Error):
"""Attempted to read more bytes from a stream than were available."""
182 changes: 182 additions & 0 deletions _gcloud_vendor/apitools/base/py/http_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env python
"""HTTP wrapper for apitools.

This library wraps the underlying http library we use, which is
currently httplib2.
"""

import collections
import httplib
import logging
import socket
import time
import urlparse

import httplib2

from _gcloud_vendor.apitools.base.py import exceptions
from _gcloud_vendor.apitools.base.py import util

__all__ = [
'GetHttp',
'MakeRequest',
'Request',
]


# 308 and 429 don't have names in httplib.
RESUME_INCOMPLETE = 308
TOO_MANY_REQUESTS = 429
_REDIRECT_STATUS_CODES = (
httplib.MOVED_PERMANENTLY,
httplib.FOUND,
httplib.SEE_OTHER,
httplib.TEMPORARY_REDIRECT,
RESUME_INCOMPLETE,
)


class Request(object):
"""Class encapsulating the data for an HTTP request."""

def __init__(self, url='', http_method='GET', headers=None, body=''):
self.url = url
self.http_method = http_method
self.headers = headers or {}
self.__body = None
self.body = body

@property
def body(self):
return self.__body

@body.setter
def body(self, value):
self.__body = value
if value is not None:
self.headers['content-length'] = str(len(self.__body))
else:
self.headers.pop('content-length', None)


# Note: currently the order of fields here is important, since we want
# to be able to pass in the result from httplib2.request.
class Response(collections.namedtuple(
'HttpResponse', ['info', 'content', 'request_url'])):
"""Class encapsulating data for an HTTP response."""
__slots__ = ()

def __len__(self):
def ProcessContentRange(content_range):
_, _, range_spec = content_range.partition(' ')
byte_range, _, _ = range_spec.partition('/')
start, _, end = byte_range.partition('-')
return int(end) - int(start) + 1

if '-content-encoding' in self.info and 'content-range' in self.info:
# httplib2 rewrites content-length in the case of a compressed
# transfer; we can't trust the content-length header in that
# case, but we *can* trust content-range, if it's present.
return ProcessContentRange(self.info['content-range'])
elif 'content-length' in self.info:
return int(self.info.get('content-length'))
elif 'content-range' in self.info:
return ProcessContentRange(self.info['content-range'])
return len(self.content)

@property
def status_code(self):
return int(self.info['status'])

@property
def retry_after(self):
if 'retry-after' in self.info:
return int(self.info['retry-after'])

@property
def is_redirect(self):
return (self.status_code in _REDIRECT_STATUS_CODES and
'location' in self.info)


def MakeRequest(http, http_request, retries=5, redirections=5):
"""Send http_request via the given http.

This wrapper exists to handle translation between the plain httplib2
request/response types and the Request and Response types above.
This will also be the hook for error/retry handling.

Args:
http: An httplib2.Http instance, or a http multiplexer that delegates to
an underlying http, for example, HTTPMultiplexer.
http_request: A Request to send.
retries: (int, default 5) Number of retries to attempt on 5XX replies.
redirections: (int, default 5) Number of redirects to follow.

Returns:
A Response object.

Raises:
InvalidDataFromServerError: if there is no response after retries.
"""
response = None
exc = None
connection_type = None
# Handle overrides for connection types. This is used if the caller
# wants control over the underlying connection for managing callbacks
# or hash digestion.
if getattr(http, 'connections', None):
url_scheme = urlparse.urlsplit(http_request.url).scheme
if url_scheme and url_scheme in http.connections:
connection_type = http.connections[url_scheme]
for retry in xrange(retries + 1):
# Note that the str() calls here are important for working around
# some funny business with message construction and unicode in
# httplib itself. See, eg,
# http://bugs.python.org/issue11898
info = None
try:
info, content = http.request(
str(http_request.url), method=str(http_request.http_method),
body=http_request.body, headers=http_request.headers,
redirections=redirections, connection_type=connection_type)
except httplib.BadStatusLine as e:
logging.error('Caught BadStatusLine from httplib, retrying: %s', e)
exc = e
except socket.error as e:
if http_request.http_method != 'GET':
raise
logging.error('Caught socket error, retrying: %s', e)
exc = e
except httplib.IncompleteRead as e:
if http_request.http_method != 'GET':
raise
logging.error('Caught IncompleteRead error, retrying: %s', e)
exc = e
if info is not None:
response = Response(info, content, http_request.url)
if (response.status_code < 500 and
response.status_code != TOO_MANY_REQUESTS and
not response.retry_after):
break
logging.info('Retrying request to url <%s> after status code %s.',
response.request_url, response.status_code)
elif isinstance(exc, httplib.IncompleteRead):
logging.info('Retrying request to url <%s> after incomplete read.',
str(http_request.url))
else:
logging.info('Retrying request to url <%s> after connection break.',
str(http_request.url))
# TODO(craigcitro): Make this timeout configurable.
if response:
time.sleep(response.retry_after or util.CalculateWaitForRetry(retry))
else:
time.sleep(util.CalculateWaitForRetry(retry))
if response is None:
raise exceptions.InvalidDataFromServerError(
'HTTP error on final retry: %s' % exc)
return response


def GetHttp():
return httplib2.Http()
Loading