diff --git a/gcloud/connection.py b/gcloud/connection.py index 14e1006e5dc2..c56a27ff8b8f 100644 --- a/gcloud/connection.py +++ b/gcloud/connection.py @@ -14,10 +14,14 @@ """ Shared implementation of connections to API servers.""" +import json from pkg_resources import get_distribution +from six.moves.urllib.parse import urlencode # pylint: disable=F0401 import httplib2 +from gcloud.exceptions import make_exception + API_BASE_URL = 'https://www.googleapis.com' """The base of the API call URL.""" @@ -87,3 +91,209 @@ def http(self): if self._credentials: self._http = self._credentials.authorize(self._http) return self._http + + +class JSONConnection(Connection): + """A connection to a Google JSON-based API. + + These APIs are discovery based. For reference: + https://developers.google.com/discovery/ + + This defines :meth:`Connection.api_request` for making a generic JSON + API request and API requests are created elsewhere. + + The class constants + * ``API_BASE_URL`` + * ``API_VERSION`` + * ``API_URL_TEMPLATE`` + must be updated by subclasses. + """ + + API_BASE_URL = None + """The base of the API call URL.""" + + API_VERSION = None + """The version of the API, used in building the API call's URL.""" + + API_URL_TEMPLATE = None + """A template for the URL of a particular API call.""" + + @classmethod + def build_api_url(cls, path, query_params=None, + api_base_url=None, api_version=None): + """Construct an API url given a few components, some optional. + + Typically, you shouldn't need to use this method. + + :type path: string + :param path: The path to the resource (ie, ``'/b/bucket-name'``). + + :type query_params: dict + :param query_params: A dictionary of keys and values to insert into + the query string of the URL. + + :type api_base_url: string + :param api_base_url: The base URL for the API endpoint. + Typically you won't have to provide this. + + :type api_version: string + :param api_version: The version of the API to call. + Typically you shouldn't provide this and instead + use the default for the library. + + :rtype: string + :returns: The URL assembled from the pieces provided. + """ + api_base_url = api_base_url or cls.API_BASE_URL + + url = cls.API_URL_TEMPLATE.format( + api_base_url=(api_base_url or cls.API_BASE_URL), + api_version=(api_version or cls.API_VERSION), + path=path) + + query_params = query_params or {} + if query_params: + url += '?' + urlencode(query_params) + + return url + + def _make_request(self, method, url, data=None, content_type=None, + headers=None): + """A low level method to send a request to the API. + + Typically, you shouldn't need to use this method. + + :type method: string + :param method: The HTTP method to use in the request. + + :type url: string + :param url: The URL to send the request to. + + :type data: string + :param data: The data to send as the body of the request. + + :type content_type: string + :param content_type: The proper MIME type of the data provided. + + :type headers: dict + :param headers: A dictionary of HTTP headers to send with the request. + + :rtype: tuple of ``response`` (a dictionary of sorts) + and ``content`` (a string). + :returns: The HTTP response object and the content of the response, + returned by :meth:`_do_request`. + """ + headers = headers or {} + headers['Accept-Encoding'] = 'gzip' + + if data: + content_length = len(str(data)) + else: + content_length = 0 + + headers['Content-Length'] = content_length + + if content_type: + headers['Content-Type'] = content_type + + headers['User-Agent'] = self.USER_AGENT + + return self._do_request(method, url, headers, data) + + def _do_request(self, method, url, headers, data): + """Low-level helper: perform the actual API request over HTTP. + + Allows batch context managers to override and defer a request. + + :type method: string + :param method: The HTTP method to use in the request. + + :type url: string + :param url: The URL to send the request to. + + :type headers: dict + :param headers: A dictionary of HTTP headers to send with the request. + + :type data: string + :param data: The data to send as the body of the request. + + :rtype: tuple of ``response`` (a dictionary of sorts) + and ``content`` (a string). + :returns: The HTTP response object and the content of the response. + """ + return self.http.request(uri=url, method=method, headers=headers, + body=data) + + def api_request(self, method, path, query_params=None, + data=None, content_type=None, + api_base_url=None, api_version=None, + expect_json=True): + """Make a request over the HTTP transport to the API. + + You shouldn't need to use this method, but if you plan to + interact with the API using these primitives, this is the + correct one to use. + + :type method: string + :param method: The HTTP method name (ie, ``GET``, ``POST``, etc). + Required. + + :type path: string + :param path: The path to the resource (ie, ``'/b/bucket-name'``). + Required. + + :type query_params: dict + :param query_params: A dictionary of keys and values to insert into + the query string of the URL. Default is + empty dict. + + :type data: string + :param data: The data to send as the body of the request. Default is + the empty string. + + :type content_type: string + :param content_type: The proper MIME type of the data provided. Default + is None. + + :type api_base_url: string + :param api_base_url: The base URL for the API endpoint. + Typically you won't have to provide this. + Default is the standard API base URL. + + :type api_version: string + :param api_version: The version of the API to call. Typically + you shouldn't provide this and instead use + the default for the library. Default is the + latest API version supported by + gcloud-python. + + :type expect_json: boolean + :param expect_json: If True, this method will try to parse the + response as JSON and raise an exception if + that cannot be done. Default is True. + + :raises: Exception if the response code is not 200 OK. + """ + url = self.build_api_url(path=path, query_params=query_params, + api_base_url=api_base_url, + api_version=api_version) + + # Making the executive decision that any dictionary + # data will be sent properly as JSON. + if data and isinstance(data, dict): + data = json.dumps(data) + content_type = 'application/json' + + response, content = self._make_request( + method=method, url=url, data=data, content_type=content_type) + + if not 200 <= response.status < 300: + raise make_exception(response, content) + + if content and expect_json: + content_type = response.get('content-type', '') + if not content_type.startswith('application/json'): + raise TypeError('Expected JSON, got %s' % content_type) + return json.loads(content) + + return content diff --git a/gcloud/pubsub/connection.py b/gcloud/pubsub/connection.py index d5be7846ca82..f492f90f6ea2 100644 --- a/gcloud/pubsub/connection.py +++ b/gcloud/pubsub/connection.py @@ -14,21 +14,11 @@ """Create / interact with gcloud pubsub connections.""" -import json - -from six.moves.urllib.parse import urlencode # pylint: disable=F0401 - from gcloud import connection as base_connection -from gcloud.exceptions import make_exception -class Connection(base_connection.Connection): - """A connection to Google Cloud Pubsub via the JSON REST API. - - This defines :meth:`Connection.api_request` for making a generic JSON - API request and API requests are created elsewhere (e.g. in - :mod:`gcloud.pubsub.api`). - """ +class Connection(base_connection.JSONConnection): + """A connection to Google Cloud Pubsub via the JSON REST API.""" API_BASE_URL = base_connection.API_BASE_URL """The base of the API call URL.""" @@ -38,184 +28,3 @@ class Connection(base_connection.Connection): API_URL_TEMPLATE = '{api_base_url}/pubsub/{api_version}{path}' """A template for the URL of a particular API call.""" - - @classmethod - def build_api_url(cls, path, query_params=None, api_base_url=None, - api_version=None): - """Construct an API url given a few components, some optional. - - Typically, you shouldn't need to use this method. - - :type path: string - :param path: The path to the resource (ie, ``'/b/bucket-name'``). - - :type query_params: dict - :param query_params: A dictionary of keys and values to insert into - the query string of the URL. - - :type api_base_url: string - :param api_base_url: The base URL for the API endpoint. - Typically you won't have to provide this. - - :type api_version: string - :param api_version: The version of the API to call. - Typically you shouldn't provide this and instead - use the default for the library. - - :rtype: string - :returns: The URL assembled from the pieces provided. - """ - api_base_url = api_base_url or cls.API_BASE_URL - - url = cls.API_URL_TEMPLATE.format( - api_base_url=(api_base_url or cls.API_BASE_URL), - api_version=(api_version or cls.API_VERSION), - path=path) - - query_params = query_params or {} - if query_params: - url += '?' + urlencode(query_params) - - return url - - def _make_request(self, method, url, data=None, content_type=None, - headers=None): - """A low level method to send a request to the API. - - Typically, you shouldn't need to use this method. - - :type method: string - :param method: The HTTP method to use in the request. - - :type url: string - :param url: The URL to send the request to. - - :type data: string - :param data: The data to send as the body of the request. - - :type content_type: string - :param content_type: The proper MIME type of the data provided. - - :type headers: dict - :param headers: A dictionary of HTTP headers to send with the request. - - :rtype: tuple of ``response`` (a dictionary of sorts) - and ``content`` (a string). - :returns: The HTTP response object and the content of the response, - returned by :meth:`_do_request`. - """ - headers = headers or {} - headers['Accept-Encoding'] = 'gzip' - - if data: - content_length = len(str(data)) - else: - content_length = 0 - - headers['Content-Length'] = content_length - - if content_type: - headers['Content-Type'] = content_type - - headers['User-Agent'] = self.USER_AGENT - - return self._do_request(method, url, headers, data) - - def _do_request(self, method, url, headers, data): - """Low-level helper: perform the actual API request over HTTP. - - Allows :class:`gcloud.pubsub.batch.Batch` to override, deferring - the request. - - :type method: string - :param method: The HTTP method to use in the request. - - :type url: string - :param url: The URL to send the request to. - - :type headers: dict - :param headers: A dictionary of HTTP headers to send with the request. - - :type data: string - :param data: The data to send as the body of the request. - - :rtype: tuple of ``response`` (a dictionary of sorts) - and ``content`` (a string). - :returns: The HTTP response object and the content of the response. - """ - return self.http.request(uri=url, method=method, headers=headers, - body=data) - - def api_request(self, method, path, query_params=None, - data=None, content_type=None, - api_base_url=None, api_version=None, - expect_json=True): - """Make a request over the HTTP transport to the Cloud Storage API. - - You shouldn't need to use this method, but if you plan to - interact with the API using these primitives, this is the - correct one to use... - - :type method: string - :param method: The HTTP method name (ie, ``GET``, ``POST``, etc). - Required. - - :type path: string - :param path: The path to the resource (ie, ``'/b/bucket-name'``). - Required. - - :type query_params: dict - :param query_params: A dictionary of keys and values to insert into - the query string of the URL. Default is - empty dict. - - :type data: string - :param data: The data to send as the body of the request. Default is - the empty string. - - :type content_type: string - :param content_type: The proper MIME type of the data provided. Default - is None. - - :type api_base_url: string - :param api_base_url: The base URL for the API endpoint. - Typically you won't have to provide this. - Default is the standard API base URL. - - :type api_version: string - :param api_version: The version of the API to call. Typically - you shouldn't provide this and instead use - the default for the library. Default is the - latest API version supported by - gcloud-python. - - :type expect_json: boolean - :param expect_json: If True, this method will try to parse the - response as JSON and raise an exception if - that cannot be done. Default is True. - - :raises: Exception if the response code is not 200 OK. - """ - url = self.build_api_url(path=path, query_params=query_params, - api_base_url=api_base_url, - api_version=api_version) - - # Making the executive decision that any dictionary - # data will be sent properly as JSON. - if data and isinstance(data, dict): - data = json.dumps(data) - content_type = 'application/json' - - response, content = self._make_request( - method=method, url=url, data=data, content_type=content_type) - - if not 200 <= response.status < 300: - raise make_exception(response, content) - - if content and expect_json: - content_type = response.get('content-type', '') - if not content_type.startswith('application/json'): - raise TypeError('Expected JSON, got %s' % content_type) - return json.loads(content) - - return content diff --git a/gcloud/pubsub/test_connection.py b/gcloud/pubsub/test_connection.py index a88817c9dc86..73f03ddcf381 100644 --- a/gcloud/pubsub/test_connection.py +++ b/gcloud/pubsub/test_connection.py @@ -24,38 +24,6 @@ def _getTargetClass(self): def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) - def test_ctor_defaults(self): - conn = self._makeOne() - self.assertEqual(conn.credentials, None) - - def test_ctor_explicit(self): - creds = object() - conn = self._makeOne(creds) - self.assertTrue(conn.credentials is creds) - - def test_http_w_existing(self): - conn = self._makeOne() - conn._http = http = object() - self.assertTrue(conn.http is http) - - def test_http_wo_creds(self): - import httplib2 - conn = self._makeOne() - self.assertTrue(isinstance(conn.http, httplib2.Http)) - - def test_http_w_creds(self): - import httplib2 - authorized = object() - - class Creds(object): - def authorize(self, http): - self._called_with = http - return authorized - creds = Creds() - conn = self._makeOne(creds) - self.assertTrue(conn.http is authorized) - self.assertTrue(isinstance(creds._called_with, httplib2.Http)) - def test_build_api_url_no_extra_query_params(self): conn = self._makeOne() URI = '/'.join([ @@ -77,187 +45,3 @@ def test_build_api_url_w_extra_query_params(self): '/'.join(['', 'pubsub', conn.API_VERSION, 'foo'])) parms = dict(parse_qsl(qs)) self.assertEqual(parms['bar'], 'baz') - - def test__make_request_no_data_no_content_type_no_headers(self): - conn = self._makeOne() - URI = 'http://example.com/test' - http = conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - '', - ) - headers, content = conn._make_request('GET', URI) - self.assertEqual(headers['status'], '200') - self.assertEqual(headers['content-type'], 'text/plain') - self.assertEqual(content, '') - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test__make_request_w_data_no_extra_headers(self): - conn = self._makeOne() - URI = 'http://example.com/test' - http = conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - '', - ) - conn._make_request('GET', URI, {}, 'application/json') - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], {}) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'Content-Type': 'application/json', - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test__make_request_w_extra_headers(self): - conn = self._makeOne() - URI = 'http://example.com/test' - http = conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - '', - ) - conn._make_request('GET', URI, headers={'X-Foo': 'foo'}) - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'X-Foo': 'foo', - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_defaults(self): - PATH = '/path/required' - conn = self._makeOne() - URI = '/'.join([ - conn.API_BASE_URL, - 'pubsub', - '%s%s' % (conn.API_VERSION, PATH), - ]) - http = conn._http = Http( - {'status': '200', 'content-type': 'application/json'}, - '{}', - ) - self.assertEqual(conn.api_request('GET', PATH), {}) - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_w_non_json_response(self): - conn = self._makeOne() - conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - 'CONTENT', - ) - - self.assertRaises(TypeError, conn.api_request, 'GET', '/') - - def test_api_request_wo_json_expected(self): - conn = self._makeOne() - conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - 'CONTENT', - ) - self.assertEqual(conn.api_request('GET', '/', expect_json=False), - 'CONTENT') - - def test_api_request_w_query_params(self): - from six.moves.urllib.parse import parse_qsl - from six.moves.urllib.parse import urlsplit - conn = self._makeOne() - http = conn._http = Http( - {'status': '200', 'content-type': 'application/json'}, - '{}', - ) - self.assertEqual(conn.api_request('GET', '/', {'foo': 'bar'}), {}) - self.assertEqual(http._called_with['method'], 'GET') - uri = http._called_with['uri'] - scheme, netloc, path, qs, _ = urlsplit(uri) - self.assertEqual('%s://%s' % (scheme, netloc), conn.API_BASE_URL) - self.assertEqual(path, - '/'.join(['', 'pubsub', conn.API_VERSION, ''])) - parms = dict(parse_qsl(qs)) - self.assertEqual(parms['foo'], 'bar') - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_w_data(self): - import json - DATA = {'foo': 'bar'} - DATAJ = json.dumps(DATA) - conn = self._makeOne() - URI = '/'.join([ - conn.API_BASE_URL, - 'pubsub', - conn.API_VERSION, - '', - ]) - http = conn._http = Http( - {'status': '200', 'content-type': 'application/json'}, - '{}', - ) - self.assertEqual(conn.api_request('POST', '/', data=DATA), {}) - self.assertEqual(http._called_with['method'], 'POST') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], DATAJ) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': len(DATAJ), - 'Content-Type': 'application/json', - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_w_404(self): - from gcloud.exceptions import NotFound - conn = self._makeOne() - conn._http = Http( - {'status': '404', 'content-type': 'text/plain'}, - '{}' - ) - self.assertRaises(NotFound, conn.api_request, 'GET', '/') - - def test_api_request_w_500(self): - from gcloud.exceptions import InternalServerError - conn = self._makeOne() - conn._http = Http( - {'status': '500', 'content-type': 'text/plain'}, - '{}', - ) - self.assertRaises(InternalServerError, conn.api_request, 'GET', '/') - - -class Http(object): - - _called_with = None - - def __init__(self, headers, content): - from httplib2 import Response - self._response = Response(headers) - self._content = content - - def request(self, **kw): - self._called_with = kw - return self._response, self._content diff --git a/gcloud/storage/blob.py b/gcloud/storage/blob.py index d1146e8e5230..d5f2874d3e13 100644 --- a/gcloud/storage/blob.py +++ b/gcloud/storage/blob.py @@ -334,17 +334,19 @@ def upload_from_file(self, file_obj, rewind=False, size=None, upload_config = _UploadConfig() # Temporary URL, until we know simple vs. resumable. - upload_url = conn.build_api_url( - path=self.bucket.path + '/o', upload=True) + base_url = conn.API_BASE_URL + '/upload' + upload_url = conn.build_api_url(api_base_url=base_url, + path=self.bucket.path + '/o') # Use apitools 'Upload' facility. request = http_wrapper.Request(upload_url, 'POST', headers) upload.ConfigureRequest(upload_config, request, url_builder) query_params = url_builder.query_params - request.url = conn.build_api_url(path=self.bucket.path + '/o', - query_params=query_params, - upload=True) + base_url = conn.API_BASE_URL + '/upload' + request.url = conn.build_api_url(api_base_url=base_url, + path=self.bucket.path + '/o', + query_params=query_params) upload.InitializeUpload(request, conn.http) # Should we be passing callbacks through from caller? We can't diff --git a/gcloud/storage/connection.py b/gcloud/storage/connection.py index 702bb8f26cee..4a0eb98c3b04 100644 --- a/gcloud/storage/connection.py +++ b/gcloud/storage/connection.py @@ -14,23 +14,11 @@ """Create / interact with gcloud storage connections.""" -import json - -from six.moves.urllib.parse import urlencode # pylint: disable=F0401 - from gcloud import connection as base_connection -from gcloud.exceptions import make_exception -class Connection(base_connection.Connection): - """A connection to Google Cloud Storage via the JSON REST API. - - This defines :meth:`Connection.api_request` for making a generic JSON - API request and API requests are created elsewhere (e.g. in - :mod:`gcloud.storage.api` and - :class:`gcloud.storage.bucket.Bucket` and - :class:`gcloud.storage.blob.Blob`). - """ +class Connection(base_connection.JSONConnection): + """A connection to Google Cloud Storage via the JSON REST API.""" API_BASE_URL = base_connection.API_BASE_URL """The base of the API call URL.""" @@ -40,189 +28,3 @@ class Connection(base_connection.Connection): API_URL_TEMPLATE = '{api_base_url}/storage/{api_version}{path}' """A template for the URL of a particular API call.""" - - @classmethod - def build_api_url(cls, path, query_params=None, api_base_url=None, - api_version=None, upload=False): - """Construct an API url given a few components, some optional. - - Typically, you shouldn't need to use this method. - - :type path: string - :param path: The path to the resource (ie, ``'/b/bucket-name'``). - - :type query_params: dict - :param query_params: A dictionary of keys and values to insert into - the query string of the URL. - - :type api_base_url: string - :param api_base_url: The base URL for the API endpoint. - Typically you won't have to provide this. - - :type api_version: string - :param api_version: The version of the API to call. - Typically you shouldn't provide this and instead - use the default for the library. - - :type upload: boolean - :param upload: True if the URL is for uploading purposes. - - :rtype: string - :returns: The URL assembled from the pieces provided. - """ - api_base_url = api_base_url or cls.API_BASE_URL - if upload: - api_base_url += '/upload' - - url = cls.API_URL_TEMPLATE.format( - api_base_url=(api_base_url or cls.API_BASE_URL), - api_version=(api_version or cls.API_VERSION), - path=path) - - query_params = query_params or {} - if query_params: - url += '?' + urlencode(query_params) - - return url - - def _make_request(self, method, url, data=None, content_type=None, - headers=None): - """A low level method to send a request to the API. - - Typically, you shouldn't need to use this method. - - :type method: string - :param method: The HTTP method to use in the request. - - :type url: string - :param url: The URL to send the request to. - - :type data: string - :param data: The data to send as the body of the request. - - :type content_type: string - :param content_type: The proper MIME type of the data provided. - - :type headers: dict - :param headers: A dictionary of HTTP headers to send with the request. - - :rtype: tuple of ``response`` (a dictionary of sorts) - and ``content`` (a string). - :returns: The HTTP response object and the content of the response, - returned by :meth:`_do_request`. - """ - headers = headers or {} - headers['Accept-Encoding'] = 'gzip' - - if data: - content_length = len(str(data)) - else: - content_length = 0 - - headers['Content-Length'] = content_length - - if content_type: - headers['Content-Type'] = content_type - - headers['User-Agent'] = self.USER_AGENT - - return self._do_request(method, url, headers, data) - - def _do_request(self, method, url, headers, data): - """Low-level helper: perform the actual API request over HTTP. - - Allows :class:`gcloud.storage.batch.Batch` to override, deferring - the request. - - :type method: string - :param method: The HTTP method to use in the request. - - :type url: string - :param url: The URL to send the request to. - - :type headers: dict - :param headers: A dictionary of HTTP headers to send with the request. - - :type data: string - :param data: The data to send as the body of the request. - - :rtype: tuple of ``response`` (a dictionary of sorts) - and ``content`` (a string). - :returns: The HTTP response object and the content of the response. - """ - return self.http.request(uri=url, method=method, headers=headers, - body=data) - - def api_request(self, method, path, query_params=None, - data=None, content_type=None, - api_base_url=None, api_version=None, - expect_json=True): - """Make a request over the HTTP transport to the Cloud Storage API. - - You shouldn't need to use this method, but if you plan to - interact with the API using these primitives, this is the - correct one to use... - - :type method: string - :param method: The HTTP method name (ie, ``GET``, ``POST``, etc). - Required. - - :type path: string - :param path: The path to the resource (ie, ``'/b/bucket-name'``). - Required. - - :type query_params: dict - :param query_params: A dictionary of keys and values to insert into - the query string of the URL. Default is - empty dict. - - :type data: string - :param data: The data to send as the body of the request. Default is - the empty string. - - :type content_type: string - :param content_type: The proper MIME type of the data provided. Default - is None. - - :type api_base_url: string - :param api_base_url: The base URL for the API endpoint. - Typically you won't have to provide this. - Default is the standard API base URL. - - :type api_version: string - :param api_version: The version of the API to call. Typically - you shouldn't provide this and instead use - the default for the library. Default is the - latest API version supported by - gcloud-python. - - :type expect_json: boolean - :param expect_json: If True, this method will try to parse the - response as JSON and raise an exception if - that cannot be done. Default is True. - - :raises: Exception if the response code is not 200 OK. - """ - url = self.build_api_url(path=path, query_params=query_params, - api_base_url=api_base_url, - api_version=api_version) - - # Making the executive decision that any dictionary - # data will be sent properly as JSON. - if data and isinstance(data, dict): - data = json.dumps(data) - content_type = 'application/json' - - response, content = self._make_request( - method=method, url=url, data=data, content_type=content_type) - - if not 200 <= response.status < 300: - raise make_exception(response, content) - - if content and expect_json: - content_type = response.get('content-type', '') - if not content_type.startswith('application/json'): - raise TypeError('Expected JSON, got %s' % content_type) - return json.loads(content) - - return content diff --git a/gcloud/storage/test_blob.py b/gcloud/storage/test_blob.py index 4cee94ba9e8d..020276aed1dc 100644 --- a/gcloud/storage/test_blob.py +++ b/gcloud/storage/test_blob.py @@ -1014,13 +1014,11 @@ def api_request(self, **kw): return result def build_api_url(self, path, query_params=None, - api_base_url=API_BASE_URL, upload=False): + api_base_url=API_BASE_URL): from six.moves.urllib.parse import urlencode from six.moves.urllib.parse import urlsplit from six.moves.urllib.parse import urlunsplit - # mimic the build_api_url interface, but avoid unused param and - # missed coverage errors - upload = not upload # pragma NO COVER + # Mimic the build_api_url interface. qs = urlencode(query_params or {}) scheme, netloc, _, _, _ = urlsplit(api_base_url) return urlunsplit((scheme, netloc, path, qs, '')) diff --git a/gcloud/storage/test_connection.py b/gcloud/storage/test_connection.py index 69785870715a..30ddef173b42 100644 --- a/gcloud/storage/test_connection.py +++ b/gcloud/storage/test_connection.py @@ -24,38 +24,6 @@ def _getTargetClass(self): def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) - def test_ctor_defaults(self): - conn = self._makeOne() - self.assertEqual(conn.credentials, None) - - def test_ctor_explicit(self): - creds = object() - conn = self._makeOne(creds) - self.assertTrue(conn.credentials is creds) - - def test_http_w_existing(self): - conn = self._makeOne() - conn._http = http = object() - self.assertTrue(conn.http is http) - - def test_http_wo_creds(self): - import httplib2 - conn = self._makeOne() - self.assertTrue(isinstance(conn.http, httplib2.Http)) - - def test_http_w_creds(self): - import httplib2 - authorized = object() - - class Creds(object): - def authorize(self, http): - self._called_with = http - return authorized - creds = Creds() - conn = self._makeOne(creds) - self.assertTrue(conn.http is authorized) - self.assertTrue(isinstance(creds._called_with, httplib2.Http)) - def test_build_api_url_no_extra_query_params(self): conn = self._makeOne() URI = '/'.join([ @@ -77,198 +45,3 @@ def test_build_api_url_w_extra_query_params(self): '/'.join(['', 'storage', conn.API_VERSION, 'foo'])) parms = dict(parse_qsl(qs)) self.assertEqual(parms['bar'], 'baz') - - def test_build_api_url_w_upload(self): - conn = self._makeOne() - URI = '/'.join([ - conn.API_BASE_URL, - 'upload', - 'storage', - conn.API_VERSION, - 'foo', - ]) - self.assertEqual(conn.build_api_url('/foo', upload=True), URI) - - def test__make_request_no_data_no_content_type_no_headers(self): - conn = self._makeOne() - URI = 'http://example.com/test' - http = conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - '', - ) - headers, content = conn._make_request('GET', URI) - self.assertEqual(headers['status'], '200') - self.assertEqual(headers['content-type'], 'text/plain') - self.assertEqual(content, '') - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test__make_request_w_data_no_extra_headers(self): - conn = self._makeOne() - URI = 'http://example.com/test' - http = conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - '', - ) - conn._make_request('GET', URI, {}, 'application/json') - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], {}) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'Content-Type': 'application/json', - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test__make_request_w_extra_headers(self): - conn = self._makeOne() - URI = 'http://example.com/test' - http = conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - '', - ) - conn._make_request('GET', URI, headers={'X-Foo': 'foo'}) - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'X-Foo': 'foo', - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_defaults(self): - PATH = '/path/required' - conn = self._makeOne() - URI = '/'.join([ - conn.API_BASE_URL, - 'storage', - '%s%s' % (conn.API_VERSION, PATH), - ]) - http = conn._http = Http( - {'status': '200', 'content-type': 'application/json'}, - '{}', - ) - self.assertEqual(conn.api_request('GET', PATH), {}) - self.assertEqual(http._called_with['method'], 'GET') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_w_non_json_response(self): - conn = self._makeOne() - conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - 'CONTENT', - ) - - self.assertRaises(TypeError, conn.api_request, 'GET', '/') - - def test_api_request_wo_json_expected(self): - conn = self._makeOne() - conn._http = Http( - {'status': '200', 'content-type': 'text/plain'}, - 'CONTENT', - ) - self.assertEqual(conn.api_request('GET', '/', expect_json=False), - 'CONTENT') - - def test_api_request_w_query_params(self): - from six.moves.urllib.parse import parse_qsl - from six.moves.urllib.parse import urlsplit - conn = self._makeOne() - http = conn._http = Http( - {'status': '200', 'content-type': 'application/json'}, - '{}', - ) - self.assertEqual(conn.api_request('GET', '/', {'foo': 'bar'}), {}) - self.assertEqual(http._called_with['method'], 'GET') - uri = http._called_with['uri'] - scheme, netloc, path, qs, _ = urlsplit(uri) - self.assertEqual('%s://%s' % (scheme, netloc), conn.API_BASE_URL) - self.assertEqual(path, - '/'.join(['', 'storage', conn.API_VERSION, ''])) - parms = dict(parse_qsl(qs)) - self.assertEqual(parms['foo'], 'bar') - self.assertEqual(http._called_with['body'], None) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': 0, - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_w_data(self): - import json - DATA = {'foo': 'bar'} - DATAJ = json.dumps(DATA) - conn = self._makeOne() - URI = '/'.join([ - conn.API_BASE_URL, - 'storage', - conn.API_VERSION, - '', - ]) - http = conn._http = Http( - {'status': '200', 'content-type': 'application/json'}, - '{}', - ) - self.assertEqual(conn.api_request('POST', '/', data=DATA), {}) - self.assertEqual(http._called_with['method'], 'POST') - self.assertEqual(http._called_with['uri'], URI) - self.assertEqual(http._called_with['body'], DATAJ) - expected_headers = { - 'Accept-Encoding': 'gzip', - 'Content-Length': len(DATAJ), - 'Content-Type': 'application/json', - 'User-Agent': conn.USER_AGENT, - } - self.assertEqual(http._called_with['headers'], expected_headers) - - def test_api_request_w_404(self): - from gcloud.exceptions import NotFound - conn = self._makeOne() - conn._http = Http( - {'status': '404', 'content-type': 'text/plain'}, - '{}' - ) - self.assertRaises(NotFound, conn.api_request, 'GET', '/') - - def test_api_request_w_500(self): - from gcloud.exceptions import InternalServerError - conn = self._makeOne() - conn._http = Http( - {'status': '500', 'content-type': 'text/plain'}, - '{}', - ) - self.assertRaises(InternalServerError, conn.api_request, 'GET', '/') - - -class Http(object): - - _called_with = None - - def __init__(self, headers, content): - from httplib2 import Response - self._response = Response(headers) - self._content = content - - def request(self, **kw): - self._called_with = kw - return self._response, self._content diff --git a/gcloud/test_connection.py b/gcloud/test_connection.py index 4c1f23baacca..2cdda517eca8 100644 --- a/gcloud/test_connection.py +++ b/gcloud/test_connection.py @@ -46,12 +46,12 @@ def test_http_w_existing(self): self.assertTrue(conn.http is http) def test_http_wo_creds(self): - from httplib2 import Http + import httplib2 conn = self._makeOne() - self.assertTrue(isinstance(conn.http, Http)) + self.assertTrue(isinstance(conn.http, httplib2.Http)) def test_http_w_creds(self): - from httplib2 import Http + import httplib2 authorized = object() @@ -62,7 +62,7 @@ def authorize(self, http): creds = Creds() conn = self._makeOne(creds) self.assertTrue(conn.http is authorized) - self.assertTrue(isinstance(creds._called_with, Http)) + self.assertTrue(isinstance(creds._called_with, httplib2.Http)) def test_user_agent_format(self): from pkg_resources import get_distribution @@ -70,3 +70,280 @@ def test_user_agent_format(self): get_distribution('gcloud').version) conn = self._makeOne() self.assertEqual(conn.USER_AGENT, expected_ua) + + +class TestJSONConnection(unittest2.TestCase): + + def _getTargetClass(self): + from gcloud.connection import JSONConnection + return JSONConnection + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def _makeMockOne(self, *args, **kw): + class MockConnection(self._getTargetClass()): + API_URL_TEMPLATE = '{api_base_url}/mock/{api_version}{path}' + API_BASE_URL = 'http://mock' + API_VERSION = 'vMOCK' + return MockConnection(*args, **kw) + + def test_class_defaults(self): + klass = self._getTargetClass() + self.assertIsNone(klass.API_URL_TEMPLATE) + self.assertIsNone(klass.API_BASE_URL) + self.assertIsNone(klass.API_VERSION) + + def test_ctor_defaults(self): + conn = self._makeOne() + self.assertEqual(conn.credentials, None) + + def test_ctor_explicit(self): + creds = object() + conn = self._makeOne(creds) + self.assertTrue(conn.credentials is creds) + + def test_http_w_existing(self): + conn = self._makeOne() + conn._http = http = object() + self.assertTrue(conn.http is http) + + def test_http_wo_creds(self): + import httplib2 + conn = self._makeOne() + self.assertTrue(isinstance(conn.http, httplib2.Http)) + + def test_http_w_creds(self): + import httplib2 + authorized = object() + + class Creds(object): + def authorize(self, http): + self._called_with = http + return authorized + creds = Creds() + conn = self._makeOne(creds) + self.assertTrue(conn.http is authorized) + self.assertTrue(isinstance(creds._called_with, httplib2.Http)) + + def test_build_api_url_no_extra_query_params(self): + conn = self._makeMockOne() + # Intended to emulate self.mock_template + URI = '/'.join([ + conn.API_BASE_URL, + 'mock', + conn.API_VERSION, + 'foo', + ]) + self.assertEqual(conn.build_api_url('/foo'), URI) + + def test_build_api_url_w_extra_query_params(self): + from six.moves.urllib.parse import parse_qsl + from six.moves.urllib.parse import urlsplit + conn = self._makeMockOne() + uri = conn.build_api_url('/foo', {'bar': 'baz'}) + + scheme, netloc, path, qs, _ = urlsplit(uri) + self.assertEqual('%s://%s' % (scheme, netloc), conn.API_BASE_URL) + # Intended to emulate mock_template + PATH = '/'.join([ + '', + 'mock', + conn.API_VERSION, + 'foo', + ]) + self.assertEqual(path, PATH) + parms = dict(parse_qsl(qs)) + self.assertEqual(parms['bar'], 'baz') + + def test__make_request_no_data_no_content_type_no_headers(self): + conn = self._makeOne() + URI = 'http://example.com/test' + http = conn._http = _Http( + {'status': '200', 'content-type': 'text/plain'}, + '', + ) + headers, content = conn._make_request('GET', URI) + self.assertEqual(headers['status'], '200') + self.assertEqual(headers['content-type'], 'text/plain') + self.assertEqual(content, '') + self.assertEqual(http._called_with['method'], 'GET') + self.assertEqual(http._called_with['uri'], URI) + self.assertEqual(http._called_with['body'], None) + expected_headers = { + 'Accept-Encoding': 'gzip', + 'Content-Length': 0, + 'User-Agent': conn.USER_AGENT, + } + self.assertEqual(http._called_with['headers'], expected_headers) + + def test__make_request_w_data_no_extra_headers(self): + conn = self._makeOne() + URI = 'http://example.com/test' + http = conn._http = _Http( + {'status': '200', 'content-type': 'text/plain'}, + '', + ) + conn._make_request('GET', URI, {}, 'application/json') + self.assertEqual(http._called_with['method'], 'GET') + self.assertEqual(http._called_with['uri'], URI) + self.assertEqual(http._called_with['body'], {}) + expected_headers = { + 'Accept-Encoding': 'gzip', + 'Content-Length': 0, + 'Content-Type': 'application/json', + 'User-Agent': conn.USER_AGENT, + } + self.assertEqual(http._called_with['headers'], expected_headers) + + def test__make_request_w_extra_headers(self): + conn = self._makeOne() + URI = 'http://example.com/test' + http = conn._http = _Http( + {'status': '200', 'content-type': 'text/plain'}, + '', + ) + conn._make_request('GET', URI, headers={'X-Foo': 'foo'}) + self.assertEqual(http._called_with['method'], 'GET') + self.assertEqual(http._called_with['uri'], URI) + self.assertEqual(http._called_with['body'], None) + expected_headers = { + 'Accept-Encoding': 'gzip', + 'Content-Length': 0, + 'X-Foo': 'foo', + 'User-Agent': conn.USER_AGENT, + } + self.assertEqual(http._called_with['headers'], expected_headers) + + def test_api_request_defaults(self): + PATH = '/path/required' + conn = self._makeMockOne() + # Intended to emulate self.mock_template + URI = '/'.join([ + conn.API_BASE_URL, + 'mock', + '%s%s' % (conn.API_VERSION, PATH), + ]) + http = conn._http = _Http( + {'status': '200', 'content-type': 'application/json'}, + '{}', + ) + self.assertEqual(conn.api_request('GET', PATH), {}) + self.assertEqual(http._called_with['method'], 'GET') + self.assertEqual(http._called_with['uri'], URI) + self.assertEqual(http._called_with['body'], None) + expected_headers = { + 'Accept-Encoding': 'gzip', + 'Content-Length': 0, + 'User-Agent': conn.USER_AGENT, + } + self.assertEqual(http._called_with['headers'], expected_headers) + + def test_api_request_w_non_json_response(self): + conn = self._makeMockOne() + conn._http = _Http( + {'status': '200', 'content-type': 'text/plain'}, + 'CONTENT', + ) + + self.assertRaises(TypeError, conn.api_request, 'GET', '/') + + def test_api_request_wo_json_expected(self): + conn = self._makeMockOne() + conn._http = _Http( + {'status': '200', 'content-type': 'text/plain'}, + 'CONTENT', + ) + self.assertEqual(conn.api_request('GET', '/', expect_json=False), + 'CONTENT') + + def test_api_request_w_query_params(self): + from six.moves.urllib.parse import parse_qsl + from six.moves.urllib.parse import urlsplit + conn = self._makeMockOne() + http = conn._http = _Http( + {'status': '200', 'content-type': 'application/json'}, + '{}', + ) + self.assertEqual(conn.api_request('GET', '/', {'foo': 'bar'}), {}) + self.assertEqual(http._called_with['method'], 'GET') + uri = http._called_with['uri'] + scheme, netloc, path, qs, _ = urlsplit(uri) + self.assertEqual('%s://%s' % (scheme, netloc), conn.API_BASE_URL) + # Intended to emulate self.mock_template + PATH = '/'.join([ + '', + 'mock', + conn.API_VERSION, + '', + ]) + self.assertEqual(path, PATH) + parms = dict(parse_qsl(qs)) + self.assertEqual(parms['foo'], 'bar') + self.assertEqual(http._called_with['body'], None) + expected_headers = { + 'Accept-Encoding': 'gzip', + 'Content-Length': 0, + 'User-Agent': conn.USER_AGENT, + } + self.assertEqual(http._called_with['headers'], expected_headers) + + def test_api_request_w_data(self): + import json + DATA = {'foo': 'bar'} + DATAJ = json.dumps(DATA) + conn = self._makeMockOne() + # Intended to emulate self.mock_template + URI = '/'.join([ + conn.API_BASE_URL, + 'mock', + conn.API_VERSION, + '', + ]) + http = conn._http = _Http( + {'status': '200', 'content-type': 'application/json'}, + '{}', + ) + self.assertEqual(conn.api_request('POST', '/', data=DATA), {}) + self.assertEqual(http._called_with['method'], 'POST') + self.assertEqual(http._called_with['uri'], URI) + self.assertEqual(http._called_with['body'], DATAJ) + expected_headers = { + 'Accept-Encoding': 'gzip', + 'Content-Length': len(DATAJ), + 'Content-Type': 'application/json', + 'User-Agent': conn.USER_AGENT, + } + self.assertEqual(http._called_with['headers'], expected_headers) + + def test_api_request_w_404(self): + from gcloud.exceptions import NotFound + conn = self._makeMockOne() + conn._http = _Http( + {'status': '404', 'content-type': 'text/plain'}, + '{}' + ) + self.assertRaises(NotFound, conn.api_request, 'GET', '/') + + def test_api_request_w_500(self): + from gcloud.exceptions import InternalServerError + conn = self._makeMockOne() + conn._http = _Http( + {'status': '500', 'content-type': 'text/plain'}, + '{}', + ) + self.assertRaises(InternalServerError, conn.api_request, 'GET', '/') + + +class _Http(object): + + _called_with = None + + def __init__(self, headers, content): + from httplib2 import Response + self._response = Response(headers) + self._content = content + + def request(self, **kw): + self._called_with = kw + return self._response, self._content