Skip to content

Commit ef50cc7

Browse files
committed
Merge pull request #400 from tseaver/334-vendor-apitools-subset-redux
#334: vendor-in apitools subset
2 parents 3b90004 + f4a53ee commit ef50cc7

File tree

10 files changed

+1181
-1
lines changed

10 files changed

+1181
-1
lines changed

_gcloud_vendor/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Dependencies "vendored in", due to dependencies, Python versions, etc.
2+
3+
Current set
4+
-----------
5+
6+
``apitools`` (pending release to PyPI, plus acceptable Python version
7+
support for its dependencies). Review before M2.
8+
"""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Package stub."""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Package stub."""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Package stub."""
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python
2+
"""Exceptions for generated client libraries."""
3+
4+
5+
class Error(Exception):
6+
"""Base class for all exceptions."""
7+
8+
9+
class TypecheckError(Error, TypeError):
10+
"""An object of an incorrect type is provided."""
11+
12+
13+
class NotFoundError(Error):
14+
"""A specified resource could not be found."""
15+
16+
17+
class UserError(Error):
18+
"""Base class for errors related to user input."""
19+
20+
21+
class InvalidDataError(Error):
22+
"""Base class for any invalid data error."""
23+
24+
25+
class CommunicationError(Error):
26+
"""Any communication error talking to an API server."""
27+
28+
29+
class HttpError(CommunicationError):
30+
"""Error making a request. Soon to be HttpError."""
31+
32+
def __init__(self, response, content, url):
33+
super(HttpError, self).__init__()
34+
self.response = response
35+
self.content = content
36+
self.url = url
37+
38+
def __str__(self):
39+
content = self.content.decode('ascii', 'replace')
40+
return 'HttpError accessing <%s>: response: <%s>, content <%s>' % (
41+
self.url, self.response, content)
42+
43+
@property
44+
def status_code(self):
45+
# TODO(craigcitro): Turn this into something better than a
46+
# KeyError if there is no status.
47+
return int(self.response['status'])
48+
49+
@classmethod
50+
def FromResponse(cls, http_response):
51+
return cls(http_response.info, http_response.content,
52+
http_response.request_url)
53+
54+
55+
class InvalidUserInputError(InvalidDataError):
56+
"""User-provided input is invalid."""
57+
58+
59+
class InvalidDataFromServerError(InvalidDataError, CommunicationError):
60+
"""Data received from the server is malformed."""
61+
62+
63+
class BatchError(Error):
64+
"""Error generated while constructing a batch request."""
65+
66+
67+
class ConfigurationError(Error):
68+
"""Base class for configuration errors."""
69+
70+
71+
class GeneratedClientError(Error):
72+
"""The generated client configuration is invalid."""
73+
74+
75+
class ConfigurationValueError(UserError):
76+
"""Some part of the user-specified client configuration is invalid."""
77+
78+
79+
class ResourceUnavailableError(Error):
80+
"""User requested an unavailable resource."""
81+
82+
83+
class CredentialsError(Error):
84+
"""Errors related to invalid credentials."""
85+
86+
87+
class TransferError(CommunicationError):
88+
"""Errors related to transfers."""
89+
90+
91+
class TransferInvalidError(TransferError):
92+
"""The given transfer is invalid."""
93+
94+
95+
class NotYetImplementedError(GeneratedClientError):
96+
"""This functionality is not yet implemented."""
97+
98+
99+
class StreamExhausted(Error):
100+
"""Attempted to read more bytes from a stream than were available."""
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
#!/usr/bin/env python
2+
"""HTTP wrapper for apitools.
3+
4+
This library wraps the underlying http library we use, which is
5+
currently httplib2.
6+
"""
7+
8+
import collections
9+
import httplib
10+
import logging
11+
import socket
12+
import time
13+
import urlparse
14+
15+
import httplib2
16+
17+
from _gcloud_vendor.apitools.base.py import exceptions
18+
from _gcloud_vendor.apitools.base.py import util
19+
20+
__all__ = [
21+
'GetHttp',
22+
'MakeRequest',
23+
'Request',
24+
]
25+
26+
27+
# 308 and 429 don't have names in httplib.
28+
RESUME_INCOMPLETE = 308
29+
TOO_MANY_REQUESTS = 429
30+
_REDIRECT_STATUS_CODES = (
31+
httplib.MOVED_PERMANENTLY,
32+
httplib.FOUND,
33+
httplib.SEE_OTHER,
34+
httplib.TEMPORARY_REDIRECT,
35+
RESUME_INCOMPLETE,
36+
)
37+
38+
39+
class Request(object):
40+
"""Class encapsulating the data for an HTTP request."""
41+
42+
def __init__(self, url='', http_method='GET', headers=None, body=''):
43+
self.url = url
44+
self.http_method = http_method
45+
self.headers = headers or {}
46+
self.__body = None
47+
self.body = body
48+
49+
@property
50+
def body(self):
51+
return self.__body
52+
53+
@body.setter
54+
def body(self, value):
55+
self.__body = value
56+
if value is not None:
57+
self.headers['content-length'] = str(len(self.__body))
58+
else:
59+
self.headers.pop('content-length', None)
60+
61+
62+
# Note: currently the order of fields here is important, since we want
63+
# to be able to pass in the result from httplib2.request.
64+
class Response(collections.namedtuple(
65+
'HttpResponse', ['info', 'content', 'request_url'])):
66+
"""Class encapsulating data for an HTTP response."""
67+
__slots__ = ()
68+
69+
def __len__(self):
70+
def ProcessContentRange(content_range):
71+
_, _, range_spec = content_range.partition(' ')
72+
byte_range, _, _ = range_spec.partition('/')
73+
start, _, end = byte_range.partition('-')
74+
return int(end) - int(start) + 1
75+
76+
if '-content-encoding' in self.info and 'content-range' in self.info:
77+
# httplib2 rewrites content-length in the case of a compressed
78+
# transfer; we can't trust the content-length header in that
79+
# case, but we *can* trust content-range, if it's present.
80+
return ProcessContentRange(self.info['content-range'])
81+
elif 'content-length' in self.info:
82+
return int(self.info.get('content-length'))
83+
elif 'content-range' in self.info:
84+
return ProcessContentRange(self.info['content-range'])
85+
return len(self.content)
86+
87+
@property
88+
def status_code(self):
89+
return int(self.info['status'])
90+
91+
@property
92+
def retry_after(self):
93+
if 'retry-after' in self.info:
94+
return int(self.info['retry-after'])
95+
96+
@property
97+
def is_redirect(self):
98+
return (self.status_code in _REDIRECT_STATUS_CODES and
99+
'location' in self.info)
100+
101+
102+
def MakeRequest(http, http_request, retries=5, redirections=5):
103+
"""Send http_request via the given http.
104+
105+
This wrapper exists to handle translation between the plain httplib2
106+
request/response types and the Request and Response types above.
107+
This will also be the hook for error/retry handling.
108+
109+
Args:
110+
http: An httplib2.Http instance, or a http multiplexer that delegates to
111+
an underlying http, for example, HTTPMultiplexer.
112+
http_request: A Request to send.
113+
retries: (int, default 5) Number of retries to attempt on 5XX replies.
114+
redirections: (int, default 5) Number of redirects to follow.
115+
116+
Returns:
117+
A Response object.
118+
119+
Raises:
120+
InvalidDataFromServerError: if there is no response after retries.
121+
"""
122+
response = None
123+
exc = None
124+
connection_type = None
125+
# Handle overrides for connection types. This is used if the caller
126+
# wants control over the underlying connection for managing callbacks
127+
# or hash digestion.
128+
if getattr(http, 'connections', None):
129+
url_scheme = urlparse.urlsplit(http_request.url).scheme
130+
if url_scheme and url_scheme in http.connections:
131+
connection_type = http.connections[url_scheme]
132+
for retry in xrange(retries + 1):
133+
# Note that the str() calls here are important for working around
134+
# some funny business with message construction and unicode in
135+
# httplib itself. See, eg,
136+
# http://bugs.python.org/issue11898
137+
info = None
138+
try:
139+
info, content = http.request(
140+
str(http_request.url), method=str(http_request.http_method),
141+
body=http_request.body, headers=http_request.headers,
142+
redirections=redirections, connection_type=connection_type)
143+
except httplib.BadStatusLine as e:
144+
logging.error('Caught BadStatusLine from httplib, retrying: %s', e)
145+
exc = e
146+
except socket.error as e:
147+
if http_request.http_method != 'GET':
148+
raise
149+
logging.error('Caught socket error, retrying: %s', e)
150+
exc = e
151+
except httplib.IncompleteRead as e:
152+
if http_request.http_method != 'GET':
153+
raise
154+
logging.error('Caught IncompleteRead error, retrying: %s', e)
155+
exc = e
156+
if info is not None:
157+
response = Response(info, content, http_request.url)
158+
if (response.status_code < 500 and
159+
response.status_code != TOO_MANY_REQUESTS and
160+
not response.retry_after):
161+
break
162+
logging.info('Retrying request to url <%s> after status code %s.',
163+
response.request_url, response.status_code)
164+
elif isinstance(exc, httplib.IncompleteRead):
165+
logging.info('Retrying request to url <%s> after incomplete read.',
166+
str(http_request.url))
167+
else:
168+
logging.info('Retrying request to url <%s> after connection break.',
169+
str(http_request.url))
170+
# TODO(craigcitro): Make this timeout configurable.
171+
if response:
172+
time.sleep(response.retry_after or util.CalculateWaitForRetry(retry))
173+
else:
174+
time.sleep(util.CalculateWaitForRetry(retry))
175+
if response is None:
176+
raise exceptions.InvalidDataFromServerError(
177+
'HTTP error on final retry: %s' % exc)
178+
return response
179+
180+
181+
def GetHttp():
182+
return httplib2.Http()

0 commit comments

Comments
 (0)