From abfec7008e6040136a16a3a0f7dc72b5579c721e Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 8 Aug 2017 10:32:00 -0700 Subject: [PATCH] Add google.api.core.exceptions (#3738) * Add google.api.core.exceptions * Add google.api.core to coverage report * Alias google.cloud.exceptions to google.api.core.exceptions * Fix lint * Address review comments * Fix typo --- core/google/api/core/exceptions.py | 420 ++++++++++++++++++++ core/google/cloud/exceptions.py | 254 ++---------- core/google/cloud/obselete.py | 2 + core/nox.py | 1 + core/tests/unit/api_core/__init__.py | 0 core/tests/unit/api_core/test_exceptions.py | 201 ++++++++++ 6 files changed, 655 insertions(+), 223 deletions(-) create mode 100644 core/google/api/core/exceptions.py create mode 100644 core/tests/unit/api_core/__init__.py create mode 100644 core/tests/unit/api_core/test_exceptions.py diff --git a/core/google/api/core/exceptions.py b/core/google/api/core/exceptions.py new file mode 100644 index 000000000000..c25816abce34 --- /dev/null +++ b/core/google/api/core/exceptions.py @@ -0,0 +1,420 @@ +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exceptions raised by Google API core & clients. + +This module provides base classes for all errors raised by libraries based +on :mod:`google.api.core`, including both HTTP and gRPC clients. +""" + +from __future__ import absolute_import +from __future__ import unicode_literals + +import six +from six.moves import http_client + +try: + import grpc +except ImportError: # pragma: NO COVER + grpc = None + +# Lookup tables for mapping exceptions from HTTP and gRPC transports. +# Populated by _APICallErrorMeta +_HTTP_CODE_TO_EXCEPTION = {} +_GRPC_CODE_TO_EXCEPTION = {} + + +class GoogleAPIError(Exception): + """Base class for all exceptions raised by Google API Clients.""" + pass + + +class _GoogleAPICallErrorMeta(type): + """Metaclass for registering GoogleAPICallError subclasses.""" + def __new__(mcs, name, bases, class_dict): + cls = type.__new__(mcs, name, bases, class_dict) + if cls.code is not None: + _HTTP_CODE_TO_EXCEPTION.setdefault(cls.code, cls) + if cls.grpc_status_code is not None: + _GRPC_CODE_TO_EXCEPTION.setdefault(cls.grpc_status_code, cls) + return cls + + +@six.python_2_unicode_compatible +@six.add_metaclass(_GoogleAPICallErrorMeta) +class GoogleAPICallError(GoogleAPIError): + """Base class for exceptions raised by calling API methods. + + Args: + message (str): The exception message. + errors (Sequence[Any]): An optional list of error details. + response (Union[requests.Request, grpc.Call]): The response or + gRPC call metadata. + """ + + code = None + """Optional[int]: The HTTP status code associated with this error. + + This may be ``None`` if the exception does not have a direct mapping + to an HTTP error. + + See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + """ + + grpc_status_code = None + """Optional[grpc.StatusCode]: The gRPC status code associated with this + error. + + This may be ``None`` if the exception does not match up to a gRPC error. + """ + + def __init__(self, message, errors=(), response=None): + super(GoogleAPICallError, self).__init__(message) + self.message = message + """str: The exception message.""" + self._errors = errors + self._response = response + + def __str__(self): + return '{} {}'.format(self.code, self.message) + + @property + def errors(self): + """Detailed error information. + + Returns: + Sequence[Any]: A list of additional error details. + """ + return list(self._errors) + + @property + def response(self): + """Optional[Union[requests.Request, grpc.Call]]: The response or + gRPC call metadata.""" + return self._response + + +class Redirection(GoogleAPICallError): + """Base class for for all redirection (HTTP 3xx) responses.""" + + +class MovedPermanently(Redirection): + """Exception mapping a ``301 Moved Permanently`` response.""" + code = http_client.MOVED_PERMANENTLY + + +class NotModified(Redirection): + """Exception mapping a ``304 Not Modified`` response.""" + code = http_client.NOT_MODIFIED + + +class TemporaryRedirect(Redirection): + """Exception mapping a ``307 Temporary Redirect`` response.""" + code = http_client.TEMPORARY_REDIRECT + + +class ResumeIncomplete(Redirection): + """Exception mapping a ``308 Resume Incomplete`` response. + + .. note:: :ref:`http_client.PERMANENT_REDIRECT` is ``308``, but Google APIs + differ in their use of this status code. + """ + code = 308 + + +class ClientError(GoogleAPICallError): + """Base class for all client error (HTTP 4xx) responses.""" + + +class BadRequest(ClientError): + """Exception mapping a ``400 Bad Request`` response.""" + code = http_client.BAD_REQUEST + + +class InvalidArgument(BadRequest): + """Exception mapping a :prop:`grpc.StatusCode.INVALID_ARGUMENT` error.""" + grpc_status_code = ( + grpc.StatusCode.INVALID_ARGUMENT if grpc is not None else None) + + +class FailedPrecondition(BadRequest): + """Exception mapping a :prop:`grpc.StatusCode.FAILED_PRECONDITION` + error.""" + grpc_status_code = ( + grpc.StatusCode.FAILED_PRECONDITION if grpc is not None else None) + + +class OutOfRange(BadRequest): + """Exception mapping a :prop:`grpc.StatusCode.OUT_OF_RANGE` error.""" + grpc_status_code = ( + grpc.StatusCode.OUT_OF_RANGE if grpc is not None else None) + + +class Unauthorized(ClientError): + """Exception mapping a ``401 Unauthorized`` response.""" + code = http_client.UNAUTHORIZED + + +class Unauthenticated(Unauthorized): + """Exception mapping a :prop:`grpc.StatusCode.UNAUTHENTICATED` error.""" + grpc_status_code = ( + grpc.StatusCode.UNAUTHENTICATED if grpc is not None else None) + + +class Forbidden(ClientError): + """Exception mapping a ``403 Forbidden`` response.""" + code = http_client.FORBIDDEN + + +class PermissionDenied(Forbidden): + """Exception mapping a :prop:`grpc.StatusCode.PERMISSION_DENIED` error.""" + grpc_status_code = ( + grpc.StatusCode.PERMISSION_DENIED if grpc is not None else None) + + +class NotFound(ClientError): + """Exception mapping a ``404 Not Found`` response or a + :prop:`grpc.StatusCode.NOT_FOUND` error.""" + code = http_client.NOT_FOUND + grpc_status_code = ( + grpc.StatusCode.NOT_FOUND if grpc is not None else None) + + +class MethodNotAllowed(ClientError): + """Exception mapping a ``405 Method Not Allowed`` response.""" + code = http_client.METHOD_NOT_ALLOWED + + +class Conflict(ClientError): + """Exception mapping a ``409 Conflict`` response.""" + code = http_client.CONFLICT + + +class AlreadyExists(Conflict): + """Exception mapping a :prop:`grpc.StatusCode.ALREADY_EXISTS` error.""" + grpc_status_code = ( + grpc.StatusCode.ALREADY_EXISTS if grpc is not None else None) + + +class Aborted(Conflict): + """Exception mapping a :prop:`grpc.StatusCode.ABORTED` error.""" + grpc_status_code = ( + grpc.StatusCode.ABORTED if grpc is not None else None) + + +class LengthRequired(ClientError): + """Exception mapping a ``411 Length Required`` response.""" + code = http_client.LENGTH_REQUIRED + + +class PreconditionFailed(ClientError): + """Exception mapping a ``412 Precondition Failed`` response.""" + code = http_client.PRECONDITION_FAILED + + +class RequestRangeNotSatisfiable(ClientError): + """Exception mapping a ``416 Request Range Not Satisfiable`` response.""" + code = http_client.REQUESTED_RANGE_NOT_SATISFIABLE + + +class TooManyRequests(ClientError): + """Exception mapping a ``429 Too Many Requests`` response.""" + # http_client does not define a constant for this in Python 2. + code = 429 + + +class ResourceExhausted(TooManyRequests): + """Exception mapping a :prop:`grpc.StatusCode.RESOURCE_EXHAUSTED` error.""" + grpc_status_code = ( + grpc.StatusCode.RESOURCE_EXHAUSTED if grpc is not None else None) + + +class Cancelled(ClientError): + """Exception mapping a :prop:`grpc.StatusCode.CANCELLED` error.""" + # This maps to HTTP status code 499. See + # https://github.com/googleapis/googleapis/blob/master/google/rpc\ + # /code.proto + code = 499 + grpc_status_code = grpc.StatusCode.CANCELLED if grpc is not None else None + + +class ServerError(GoogleAPICallError): + """Base for 5xx responses.""" + + +class InternalServerError(ServerError): + """Exception mapping a ``500 Internal Server Error`` response. or a + :prop:`grpc.StatusCode.INTERNAL` error.""" + code = http_client.INTERNAL_SERVER_ERROR + grpc_status_code = grpc.StatusCode.INTERNAL if grpc is not None else None + + +class Unknown(ServerError): + """Exception mapping a :prop:`grpc.StatusCode.UNKNOWN` error.""" + grpc_status_code = grpc.StatusCode.UNKNOWN if grpc is not None else None + + +class DataLoss(ServerError): + """Exception mapping a :prop:`grpc.StatusCode.DATA_LOSS` error.""" + grpc_status_code = grpc.StatusCode.DATA_LOSS if grpc is not None else None + + +class MethodNotImplemented(ServerError): + """Exception mapping a ``501 Not Implemented`` response or a + :prop:`grpc.StatusCode.UNIMPLEMENTED` error.""" + code = http_client.NOT_IMPLEMENTED + grpc_status_code = ( + grpc.StatusCode.UNIMPLEMENTED if grpc is not None else None) + + +class BadGateway(ServerError): + """Exception mapping a ``502 Bad Gateway`` response.""" + code = http_client.BAD_GATEWAY + + +class ServiceUnavailable(ServerError): + """Exception mapping a ``503 Service Unavailable`` response or a + :prop:`grpc.StatusCode.UNAVAILABLE` error.""" + code = http_client.SERVICE_UNAVAILABLE + grpc_status_code = ( + grpc.StatusCode.UNAVAILABLE if grpc is not None else None) + + +class GatewayTimeout(ServerError): + """Exception mapping a ``504 Gateway Timeout`` response.""" + code = http_client.GATEWAY_TIMEOUT + + +class DeadlineExceeded(GatewayTimeout): + """Exception mapping a :prop:`grpc.StatusCode.DEADLINE_EXCEEDED` error.""" + grpc_status_code = ( + grpc.StatusCode.DEADLINE_EXCEEDED if grpc is not None else None) + + +def exception_class_for_http_status(status_code): + """Return the exception class for a specific HTTP status code. + + Args: + status_code (int): The HTTP status code. + + Returns: + type: the appropriate subclass of :class:`GoogleAPICallError`. + """ + return _HTTP_CODE_TO_EXCEPTION.get(status_code, GoogleAPICallError) + + +def from_http_status(status_code, message, **kwargs): + """Create a :class:`GoogleAPICallError` from an HTTP status code. + + Args: + status_code (int): The HTTP status code. + message (str): The exception message. + kwargs: Additional arguments passed to the :class:`GoogleAPICallError` + constructor. + + Returns: + GoogleAPICallError: An instance of the appropriate subclass of + :class:`GoogleAPICallError`. + """ + error_class = exception_class_for_http_status(status_code) + error = error_class(message, **kwargs) + + if error.code is None: + error.code = status_code + + return error + + +def from_http_response(response): + """Create a :class:`GoogleAPICallError` from a :class:`requests.Response`. + + Args: + response (requests.Response): The HTTP response. + + Returns: + GoogleAPICallError: An instance of the appropriate subclass of + :class:`GoogleAPICallError`, with the message and errors populated + from the response. + """ + try: + payload = response.json() + except ValueError: + payload = {'error': {'message': response.text or 'unknown error'}} + + error_message = payload.get('error', {}).get('message', 'unknown error') + errors = payload.get('error', {}).get('errors', ()) + + message = '{method} {url}: {error}'.format( + method=response.request.method, + url=response.request.url, + error=error_message) + + exception = from_http_status( + response.status_code, message, errors=errors, response=response) + return exception + + +def exception_class_for_grpc_status(status_code): + """Return the exception class for a specific :class:`grpc.StatusCode`. + + Args: + status_code (grpc.StatusCode): The gRPC status code. + + Returns: + type: the appropriate subclass of :class:`GoogleAPICallError`. + """ + return _GRPC_CODE_TO_EXCEPTION.get(status_code, GoogleAPICallError) + + +def from_grpc_status(status_code, message, **kwargs): + """Create a :class:`GoogleAPICallError` from a :class:`grpc.StatusCode`. + + Args: + status_code (grpc.StatusCode): The gRPC status code. + message (str): The exception message. + kwargs: Additional arguments passed to the :class:`GoogleAPICallError` + constructor. + + Returns: + GoogleAPICallError: An instance of the appropriate subclass of + :class:`GoogleAPICallError`. + """ + error_class = exception_class_for_grpc_status(status_code) + error = error_class(message, **kwargs) + + if error.grpc_status_code is None: + error.grpc_status_code = status_code + + return error + + +def from_grpc_error(rpc_exc): + """Create a :class:`GoogleAPICallError` from a :class:`grpc.RpcError`. + + Args: + rpc_exc (grpc.RpcError): The gRPC error. + + Returns: + GoogleAPICallError: An instance of the appropriate subclass of + :class:`GoogleAPICallError`. + """ + if isinstance(rpc_exc, grpc.Call): + return from_grpc_status( + rpc_exc.code(), + rpc_exc.details(), + errors=(rpc_exc,), + response=rpc_exc) + else: + return GoogleAPICallError( + str(rpc_exc), errors=(rpc_exc,), response=rpc_exc) diff --git a/core/google/cloud/exceptions.py b/core/google/cloud/exceptions.py index 2e7eca3be98d..a5d82be30452 100644 --- a/core/google/cloud/exceptions.py +++ b/core/google/cloud/exceptions.py @@ -12,240 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Custom exceptions for :mod:`google.cloud` package. +# pylint: disable=invalid-name +# pylint recognizies all of these aliases as constants and thinks they have +# invalid names. -See https://cloud.google.com/storage/docs/json_api/v1/status-codes -""" +"""Custom exceptions for :mod:`google.cloud` package.""" # Avoid the grpc and google.cloud.grpc collision. from __future__ import absolute_import -import copy - -import six - -from google.cloud._helpers import _to_bytes +from google.api.core import exceptions try: from grpc._channel import _Rendezvous except ImportError: # pragma: NO COVER _Rendezvous = None -_HTTP_CODE_TO_EXCEPTION = {} # populated at end of module - - -# pylint: disable=invalid-name GrpcRendezvous = _Rendezvous """Exception class raised by gRPC stable.""" -# pylint: enable=invalid-name - - -class GoogleCloudError(Exception): - """Base error class for Google Cloud errors (abstract). - - Each subclass represents a single type of HTTP error response. - """ - code = None - """HTTP status code. Concrete subclasses *must* define. - - See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - """ - - def __init__(self, message, errors=()): - super(GoogleCloudError, self).__init__(message) - self.message = message - self._errors = errors - - def __str__(self): - result = u'%d %s' % (self.code, self.message) - if six.PY2: - result = _to_bytes(result, 'utf-8') - return result - - @property - def errors(self): - """Detailed error information. - - :rtype: list(dict) - :returns: a list of mappings describing each error. - """ - return [copy.deepcopy(error) for error in self._errors] - - -class Redirection(GoogleCloudError): - """Base for 3xx responses - - This class is abstract. - """ - - -class MovedPermanently(Redirection): - """Exception mapping a '301 Moved Permanently' response.""" - code = 301 - - -class NotModified(Redirection): - """Exception mapping a '304 Not Modified' response.""" - code = 304 - - -class TemporaryRedirect(Redirection): - """Exception mapping a '307 Temporary Redirect' response.""" - code = 307 - - -class ResumeIncomplete(Redirection): - """Exception mapping a '308 Resume Incomplete' response.""" - code = 308 - - -class ClientError(GoogleCloudError): - """Base for 4xx responses - - This class is abstract - """ - - -class BadRequest(ClientError): - """Exception mapping a '400 Bad Request' response.""" - code = 400 - - -class Unauthorized(ClientError): - """Exception mapping a '401 Unauthorized' response.""" - code = 401 - - -class Forbidden(ClientError): - """Exception mapping a '403 Forbidden' response.""" - code = 403 - - -class NotFound(ClientError): - """Exception mapping a '404 Not Found' response.""" - code = 404 - - -class MethodNotAllowed(ClientError): - """Exception mapping a '405 Method Not Allowed' response.""" - code = 405 - - -class Conflict(ClientError): - """Exception mapping a '409 Conflict' response.""" - code = 409 - - -class LengthRequired(ClientError): - """Exception mapping a '411 Length Required' response.""" - code = 411 - - -class PreconditionFailed(ClientError): - """Exception mapping a '412 Precondition Failed' response.""" - code = 412 - - -class RequestRangeNotSatisfiable(ClientError): - """Exception mapping a '416 Request Range Not Satisfiable' response.""" - code = 416 - - -class TooManyRequests(ClientError): - """Exception mapping a '429 Too Many Requests' response.""" - code = 429 - - -class ServerError(GoogleCloudError): - """Base for 5xx responses: (abstract)""" - - -class InternalServerError(ServerError): - """Exception mapping a '500 Internal Server Error' response.""" - code = 500 - - -class MethodNotImplemented(ServerError): - """Exception mapping a '501 Not Implemented' response.""" - code = 501 - - -class BadGateway(ServerError): - """Exception mapping a '502 Bad Gateway' response.""" - code = 502 - - -class ServiceUnavailable(ServerError): - """Exception mapping a '503 Service Unavailable' response.""" - code = 503 - - -class GatewayTimeout(ServerError): - """Exception mapping a `504 Gateway Timeout'` response.""" - code = 504 - - -def from_http_status(status_code, message, errors=()): - """Create a :class:`GoogleCloudError` from an HTTP status code. - - Args: - status_code (int): The HTTP status code. - message (str): The exception message. - errors (Sequence[Any]): A list of additional error information. - - Returns: - GoogleCloudError: An instance of the appropriate subclass of - :class:`GoogleCloudError`. - """ - error_class = _HTTP_CODE_TO_EXCEPTION.get(status_code, GoogleCloudError) - error = error_class(message, errors) - - if error.code is None: - error.code = status_code - - return error - - -def from_http_response(response): - """Create a :class:`GoogleCloudError` from a :class:`requests.Response`. - - Args: - response (requests.Response): The HTTP response. - - Returns: - GoogleCloudError: An instance of the appropriate subclass of - :class:`GoogleCloudError`, with the message and errors populated - from the response. - """ - try: - payload = response.json() - except ValueError: - payload = {'error': {'message': response.text or 'unknown error'}} - - error_message = payload.get('error', {}).get('message', 'unknown error') - errors = payload.get('error', {}).get('errors', ()) - - message = '{method} {url}: {error}'.format( - method=response.request.method, - url=response.request.url, - error=error_message) - - exception = from_http_status( - response.status_code, message, errors=errors) - exception.response = response - return exception - - -def _walk_subclasses(klass): - """Recursively walk subclass tree.""" - for sub in klass.__subclasses__(): - yield sub - for subsub in _walk_subclasses(sub): - yield subsub - -# Build the code->exception class mapping. -for _eklass in _walk_subclasses(GoogleCloudError): - code = getattr(_eklass, 'code', None) - if code is not None: - _HTTP_CODE_TO_EXCEPTION[code] = _eklass +# Aliases to moved classes. +GoogleCloudError = exceptions.GoogleAPICallError +Redirection = exceptions.Redirection +MovedPermanently = exceptions.MovedPermanently +NotModified = exceptions.NotModified +TemporaryRedirect = exceptions.TemporaryRedirect +ResumeIncomplete = exceptions.ResumeIncomplete +ClientError = exceptions.ClientError +BadRequest = exceptions.BadRequest +Unauthorized = exceptions.Unauthorized +Forbidden = exceptions.Forbidden +NotFound = exceptions.NotFound +MethodNotAllowed = exceptions.MethodNotAllowed +Conflict = exceptions.Conflict +LengthRequired = exceptions.LengthRequired +PreconditionFailed = exceptions.PreconditionFailed +RequestRangeNotSatisfiable = exceptions.RequestRangeNotSatisfiable +TooManyRequests = exceptions.TooManyRequests +ServerError = exceptions.ServerError +InternalServerError = exceptions.InternalServerError +MethodNotImplemented = exceptions.MethodNotImplemented +BadGateway = exceptions.BadGateway +ServiceUnavailable = exceptions.ServiceUnavailable +GatewayTimeout = exceptions.GatewayTimeout +from_http_status = exceptions.from_http_status +from_http_response = exceptions.from_http_response diff --git a/core/google/cloud/obselete.py b/core/google/cloud/obselete.py index 9af28cd85d52..cd70025946f7 100644 --- a/core/google/cloud/obselete.py +++ b/core/google/cloud/obselete.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Helpers for deprecated code and modules.""" + import warnings import pkg_resources diff --git a/core/nox.py b/core/nox.py index 1dca10eb9b69..b795ddfce7a6 100644 --- a/core/nox.py +++ b/core/nox.py @@ -43,6 +43,7 @@ def unit_tests(session, python_version): 'py.test', '--quiet', '--cov=google.cloud', + '--cov=google.api.core', '--cov=tests.unit', '--cov-append', '--cov-config=.coveragerc', diff --git a/core/tests/unit/api_core/__init__.py b/core/tests/unit/api_core/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/core/tests/unit/api_core/test_exceptions.py b/core/tests/unit/api_core/test_exceptions.py new file mode 100644 index 000000000000..f29873e7b3d8 --- /dev/null +++ b/core/tests/unit/api_core/test_exceptions.py @@ -0,0 +1,201 @@ +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import grpc +import mock +import requests +from six.moves import http_client + +from google.api.core import exceptions + + +def test_create_google_cloud_error(): + exception = exceptions.GoogleAPICallError('Testing') + exception.code = 600 + assert str(exception) == '600 Testing' + assert exception.message == 'Testing' + assert exception.errors == [] + assert exception.response is None + + +def test_create_google_cloud_error_with_args(): + error = { + 'domain': 'global', + 'location': 'test', + 'locationType': 'testing', + 'message': 'Testing', + 'reason': 'test', + } + response = mock.sentinel.response + exception = exceptions.GoogleAPICallError( + 'Testing', [error], response=response) + exception.code = 600 + assert str(exception) == '600 Testing' + assert exception.message == 'Testing' + assert exception.errors == [error] + assert exception.response == response + + +def test_from_http_status(): + message = 'message' + exception = exceptions.from_http_status(http_client.NOT_FOUND, message) + assert exception.code == http_client.NOT_FOUND + assert exception.message == message + assert exception.errors == [] + + +def test_from_http_status_with_errors_and_response(): + message = 'message' + errors = ['1', '2'] + response = mock.sentinel.response + exception = exceptions.from_http_status( + http_client.NOT_FOUND, message, errors=errors, response=response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == message + assert exception.errors == errors + assert exception.response == response + + +def test_from_http_status_unknown_code(): + message = 'message' + status_code = 156 + exception = exceptions.from_http_status(status_code, message) + assert exception.code == status_code + assert exception.message == message + + +def make_response(content): + response = requests.Response() + response._content = content + response.status_code = http_client.NOT_FOUND + response.request = requests.Request( + method='POST', url='https://example.com').prepare() + return response + + +def test_from_http_response_no_content(): + response = make_response(None) + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: unknown error' + assert exception.response == response + + +def test_from_http_response_text_content(): + response = make_response(b'message') + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: message' + + +def test_from_http_response_json_content(): + response = make_response(json.dumps({ + 'error': { + 'message': 'json message', + 'errors': ['1', '2'] + } + }).encode('utf-8')) + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: json message' + assert exception.errors == ['1', '2'] + + +def test_from_http_response_bad_json_content(): + response = make_response(json.dumps({'meep': 'moop'}).encode('utf-8')) + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: unknown error' + + +def test_from_grpc_status(): + message = 'message' + exception = exceptions.from_grpc_status( + grpc.StatusCode.OUT_OF_RANGE, message) + assert isinstance(exception, exceptions.BadRequest) + assert isinstance(exception, exceptions.OutOfRange) + assert exception.code == http_client.BAD_REQUEST + assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE + assert exception.message == message + assert exception.errors == [] + + +def test_from_grpc_status_with_errors_and_response(): + message = 'message' + response = mock.sentinel.response + errors = ['1', '2'] + exception = exceptions.from_grpc_status( + grpc.StatusCode.OUT_OF_RANGE, message, + errors=errors, response=response) + + assert isinstance(exception, exceptions.OutOfRange) + assert exception.message == message + assert exception.errors == errors + assert exception.response == response + + +def test_from_grpc_status_unknown_code(): + message = 'message' + exception = exceptions.from_grpc_status( + grpc.StatusCode.OK, message) + assert exception.grpc_status_code == grpc.StatusCode.OK + assert exception.message == message + + +def test_from_grpc_error(): + message = 'message' + error = mock.create_autospec(grpc.Call, instance=True) + error.code.return_value = grpc.StatusCode.INVALID_ARGUMENT + error.details.return_value = message + + exception = exceptions.from_grpc_error(error) + + assert isinstance(exception, exceptions.BadRequest) + assert isinstance(exception, exceptions.InvalidArgument) + assert exception.code == http_client.BAD_REQUEST + assert exception.grpc_status_code == grpc.StatusCode.INVALID_ARGUMENT + assert exception.message == message + assert exception.errors == [error] + assert exception.response == error + + +def test_from_grpc_error_non_call(): + message = 'message' + error = mock.create_autospec(grpc.RpcError, instance=True) + error.__str__.return_value = message + + exception = exceptions.from_grpc_error(error) + + assert isinstance(exception, exceptions.GoogleAPICallError) + assert exception.code is None + assert exception.grpc_status_code is None + assert exception.message == message + assert exception.errors == [error] + assert exception.response == error