class HTTPError(Exception):
    """
    Base HTTP Error Class
    """

    def __init__(self, response):
        self.response = response
        self.status_code = response.status_code

        message = self._best_available_message(response)
        super(HTTPError, self).__init__(message)

    def _default_error_message(self):
        return "The following error was received: {0}".format(self.response.text)

    def _handle_details(self, details):
        return "{0}".format(details)

    def _has_additional_error_info(self):
        return False

    def _additional_error_info(self):
        return []

    def _best_available_message(self, response):
        from .utils import json_error_class

        response_json = None
        error_message = [
          "HTTP status code: {0}".format(self.status_code),
        ]
        try:
            response_json = response.json()

            message = response_json.get('message', None)
            details = response_json.get('details', None)
            request_id = response_json.get('requestId', None)

            if message is not None:
                error_message.append("Message: {0}".format(message))
            else:
                error_message.append("Message: {0}".format(self._default_error_message()))
            if details is not None:
                error_message.append("Details: {0}".format(self._handle_details(details)))
            if request_id is not None:
                error_message.append("Request ID: {0}".format(request_id))
        except json_error_class():
            error_message.append("Message: {0}".format(self._default_error_message()))

        if self._has_additional_error_info():
            error_message += self._additional_error_info()

        return "\n".join(error_message) class NotFoundError(HTTPError):
    """
    404
    """

    def _default_error_message(self):
        return "The requested resource or endpoint could not be found."

    def _handle_details(self, details):
        from .utils import string_class
        if isinstance(details, string_class()):
            return details

        resource_type = None
        resource_id = None
        if 'sys' in details:
            resource_type = details['sys'].get('type', None)
            resource_id = details['sys'].get('id', None)
        else:
            resource_type = details['type']
            resource_id = details.get('id', None)

        message = "The requested {0} could not be found.".format(resource_type)
        if resource_id is not None:
            message += " ID: {0}.".format(resource_id)

        return message class BadRequestError(HTTPError):
    """
    400
    """

    def _default_error_message(self):
        return "The request was malformed or missing a required parameter."

    def _handle_details(self, details):
        from .utils import string_class
        if isinstance(details, string_class()):
            return details

        def _handle_detail(detail):
            if isinstance(detail, string_class()):
                return detail
            return detail.get('details', None)

        inner_details = [_handle_detail(detail) for detail in details['errors']]
        inner_details = [detail for detail in inner_details if detail is not None]  # This works in both Py2 and Py3
        return "\n\t".join(inner_details) class AccessDeniedError(HTTPError):
    """
    403
    """

    def _default_error_message(self):
        return "The specified token does not have access to the requested resource."

    def _handle_details(self, details):
        return "\n\tReasons:\n\t\t{0}".format("\n\t\t".join(details['reasons'])) class UnauthorizedError(HTTPError):
    """
    401
    """

    def _default_error_message(self):
        return "The authorization token was invalid." class RateLimitExceededError(HTTPError):
    """
    429
    """

    RATE_LIMIT_RESET_HEADER_KEY = 'x-contentful-ratelimit-reset'

    def _has_reset_time(self):
        return self.RATE_LIMIT_RESET_HEADER_KEY in self.response.headers

    def reset_time(self):
        """Returns the reset time in seconds until next available request."""

        return int(self.response.headers[
            self.RATE_LIMIT_RESET_HEADER_KEY
        ])

    def _has_additional_error_info(self):
        return self._has_reset_time()

    def _additional_error_info(self):
        return ["Time until reset (seconds): {0}".format(self.reset_time())]

    def _default_error_message(self):
        return "Rate limit exceeded. Too many requests." class ServerError(HTTPError):
    """
    500
    """

    def _default_error_message(self):
        return "Internal server error." class ServiceUnavailableError(HTTPError):
    """
    503
    """

    def _default_error_message(self):
        return "The request was malformed or missing a required parameter." def get_error(response):
    """Gets Error by HTTP Status Code"""

    errors = {
        400: BadRequestError,
        401: UnauthorizedError,
        403: AccessDeniedError,
        404: NotFoundError,
        429: RateLimitExceededError,
        500: ServerError,
        502: BadGatewayError,
        503: ServiceUnavailableError
    }

    error_class = HTTPError
    if response.status_code in errors:
        error_class = errors[response.status_code]

    return error_class(response) class retry_request(object):
    """
    Decorator to retry function calls in case they raise rate limit exceptions
    """

    def __init__(self, client):
        self.client = client

    def __call__(self, http_call):
        def wrapper(url, query=None):
            exception = None
            for i in range(self.client.max_rate_limit_retries + 1):
                try:
                    return http_call(url, query)
                except RateLimitExceededError as error:
                    exception = error
                    reset_time = error.reset_time()

                    if reset_time > self.client.max_rate_limit_wait:
                        raise error

                    retry_message = 'Contentful API Rate Limit Hit! '
                    retry_message += "Retrying - Retries left: {0} ".format(
                        self.client.max_rate_limit_retries - i
                    )
                    retry_message += "- Time until reset (seconds): {0}".format(
                        reset_time
                    )
                    log.debug(retry_message)
                    time.sleep(reset_time * uniform(1.0, 1.2))
            if exception is not None:
                raise exception
        return wrapper def json_error_class():
    """Returns the class for JSON decode errors
    depends on the Python version."""
    if sys.version_info[0] >= 3 and sys.version_info[1] >= 5:
        return json.JSONDecodeError
    return ValueError class Client(object):
    """Constructs the API Client.

    :param space_id: Space ID of your target space.
    :param access_token: API Access Token (Delivery by default,
        Preview if overriding api_url).
    :param api_url: (optional) URL of the Contentful Target API,
        defaults to Delivery API (can be overriden for Preview API).
    :param api_version: (optional) Target version of the Contentful API.
    :param default_locale: (optional) Default Locale for your Space,
        defaults to 'en-US'.
    :param environment: (optional) Default Environment for client, defaults
        to 'master'.
    :param https: (optional) Boolean determining wether to use https
        or http, defaults to True.
    :param authorization_as_header: (optional) Boolean determining wether
        to send access_token through a header or via GET params,
        defaults to True.
    :param raw_mode: (optional) Boolean determining wether to process the
        response or return it raw after each API call, defaults to False.
    :param gzip_encoded: (optional) Boolean determining wether to accept
        gzip encoded results, defaults to True.
    :param raise_errors: (optional) Boolean determining wether to raise
        an exception on requests that aren't successful, defaults to True.
    :param content_type_cache: (optional) Boolean determining wether to
        store a Cache of the Content Types in order to properly coerce
        Entry fields, defaults to True.
    :param reuse_entries: (optional) Boolean determining wether to reuse
        hydrated Entry and Asset objects within the same request when possible.
        Defaults to False
    :param timeout_s: (optional) Max time allowed for each API call, in seconds.
        Defaults to 1s.
    :param proxy_host: (optional) URL for Proxy, defaults to None.
    :param proxy_port: (optional) Port for Proxy, defaults to None.
    :param proxy_username: (optional) Username for Proxy, defaults to None.
    :param proxy_password: (optional) Password for Proxy, defaults to None.
    :param max_rate_limit_retries: (optional) Maximum amount of retries
        after RateLimitError, defaults to 1.
    :param max_rate_limit_wait: (optional) Timeout (in seconds) for waiting
        for retry after RateLimitError, defaults to 60.
    :param max_include_resolution_depth: (optional) Maximum include resolution
        level for Resources, defaults to 20 (max include level * 2).
    :param application_name: (optional) User application name, defaults to None.
    :param application_version: (optional) User application version, defaults to None.
    :param integration_name: (optional) Integration name, defaults to None.
    :param integration_version: (optional) Integration version, defaults to None.
    :return: :class:`Client <Client>` object.
    :rtype: contentful.Client

    Usage:

        >>> import contentful
        >>> client = contentful.Client('cfexampleapi', 'b4c0n73n7fu1')
        <contentful.Client space_id="cfexampleapi"
          access_token="b4c0n73n7fu1"
          default_locale="en-US">
    """

    def __init__(
            self,
            space_id,
            access_token,
            api_url='cdn.contentful.com',
            api_version=1,
            default_locale='en-US',
            environment='master',
            https=True,
            authorization_as_header=True,
            raw_mode=False,
            gzip_encoded=True,
            raise_errors=True,
            content_type_cache=True,
            reuse_entries=False,
            timeout_s=1,
            proxy_host=None,
            proxy_port=None,
            proxy_username=None,
            proxy_password=None,
            max_rate_limit_retries=1,
            max_rate_limit_wait=60,
            max_include_resolution_depth=20,
            application_name=None,
            application_version=None,
            integration_name=None,
            integration_version=None):
        self.space_id = space_id
        self.access_token = access_token
        self.api_url = api_url
        self.api_version = api_version
        self.default_locale = default_locale
        self.environment = environment
        self.https = https
        self.authorization_as_header = authorization_as_header
        self.raw_mode = raw_mode
        self.gzip_encoded = gzip_encoded
        self.raise_errors = raise_errors
        self.content_type_cache = content_type_cache
        self.reuse_entries = reuse_entries
        self.timeout_s = timeout_s
        self.proxy_host = proxy_host
        self.proxy_port = proxy_port
        self.proxy_username = proxy_username
        self.proxy_password = proxy_password
        self.max_rate_limit_retries = max_rate_limit_retries
        self.max_rate_limit_wait = max_rate_limit_wait
        self.max_include_resolution_depth = max_include_resolution_depth
        self.application_name = application_name
        self.application_version = application_version
        self.integration_name = integration_name
        self.integration_version = integration_version

        self._validate_configuration()
        if self.content_type_cache:
            self._cache_content_types()

    def space(self, query=None):
        """Fetches the current Space.

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/spaces/get-a-space

        :param query: (optional) Dict with API options.
        :return: :class:`Space <contentful.space.Space>` object.
        :rtype: contentful.space.Space

        Usage:

            >>> space = client.space()
            <Space[Contentful Example API] id='cfexampleapi'>
        """

        return self._get('', query)

    def content_type(self, content_type_id, query=None):
        """Fetches a Content Type by ID.

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/content-types/content-type/get-a-single-content-type

        :param content_type_id: The ID of the target Content Type.
        :param query: (optional) Dict with API options.
        :return: :class:`ContentType <contentful.content_type.ContentType>` object.
        :rtype: contentful.content_type.ContentType

        Usage:
            >>> cat_content_type = client.content_type('cat')
            <ContentType[Cat] id='cat'>
        """

        return self._get(
            self.environment_url(
                '/content_types/{0}'.format(content_type_id)
            ),
            query
        )

    def content_types(self, query=None):
        """Fetches all Content Types from the Space.

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/content-types/content-model/get-the-content-model-of-a-space

        :param query: (optional) Dict with API options.
        :return: List of :class:`ContentType <contentful.content_type.ContentType>` objects.
        :rtype: List of contentful.content_type.ContentType

        Usage:
            >>> content_types = client.content_types()
            [<ContentType[City] id='1t9IbcfdCk6m04uISSsaIK'>,
             <ContentType[Human] id='human'>,
             <ContentType[Dog] id='dog'>,
             <ContentType[Cat] id='cat'>]
        """

        return self._get(
            self.environment_url('/content_types'),
            query
        )

    def entry(self, entry_id, query=None):
        """Fetches an Entry by ID.

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entry/get-a-single-entry

        :param entry_id: The ID of the target Entry.
        :param query: (optional) Dict with API options.
        :return: :class:`Entry <contentful.entry.Entry>` object.
        :rtype: contentful.entry.Entry

        Usage:
            >>> nyancat_entry = client.entry('nyancat')
            <Entry[cat] id='nyancat'>
        """

        if query is None:
            query = {}
        self._normalize_select(query)

        try:
            query.update({'sys.id': entry_id})
            response = self._get(
                self.environment_url('/entries'),
                query
            )

            if self.raw_mode:
                return response
            return response[0]
        except IndexError:
            raise EntryNotFoundError(
                "Entry not found for ID: '{0}'".format(entry_id)
            )

    def entries(self, query=None):
        """Fetches all Entries from the Space (up to the set limit, can be modified in `query`).

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection/get-all-entries-of-a-space

        :param query: (optional) Dict with API options.
        :return: List of :class:`Entry <contentful.entry.Entry>` objects.
        :rtype: List of contentful.entry.Entry

        Usage:
            >>> entries = client.entries()
            [<Entry[cat] id='happycat'>,
             <Entry[1t9IbcfdCk6m04uISSsaIK] id='5ETMRzkl9KM4omyMwKAOki'>,
             <Entry[dog] id='6KntaYXaHSyIw8M6eo26OK'>,
             <Entry[1t9IbcfdCk6m04uISSsaIK] id='7qVBlCjpWE86Oseo40gAEY'>,
             <Entry[cat] id='garfield'>,
             <Entry[1t9IbcfdCk6m04uISSsaIK] id='4MU1s3potiUEM2G4okYOqw'>,
             <Entry[cat] id='nyancat'>,
             <Entry[1t9IbcfdCk6m04uISSsaIK] id='ge1xHyH3QOWucKWCCAgIG'>,
             <Entry[human] id='finn'>,
             <Entry[dog] id='jake'>]
        """

        if query is None:
            query = {}
        self._normalize_select(query)

        return self._get(
            self.environment_url('/entries'),
            query
        )

    def asset(self, asset_id, query=None):
        """Fetches an Asset by ID.

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets/asset/get-a-single-asset

        :param asset_id: The ID of the target Asset.
        :param query: (optional) Dict with API options.
        :return: :class:`Asset <Asset>` object.
        :rtype: contentful.asset.Asset

        Usage:
            >>> nyancat_asset = client.asset('nyancat')
            <Asset id='nyancat' url='//images.contentful.com/cfex...'>
        """

        return self._get(
            self.environment_url(
                '/assets/{0}'.format(asset_id)
            ),
            query
        )

    def assets(self, query=None):
        """Fetches all Assets from the Space (up to the set limit, can be modified in `query`).

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets/assets-collection/get-all-assets-of-a-space

        :param query: (optional) Dict with API options.
        :return: List of :class:`Asset <contentful.asset.Asset>` objects.
        :rtype: List of contentful.asset.Asset

        Usage:
            >>> assets = client.assets()
            [<Asset id='1x0xpXu4pSGS4OukSyWGUK' url='//images.content...'>,
             <Asset id='happycat' url='//images.contentful.com/cfexam...'>,
             <Asset id='nyancat' url='//images.contentful.com/cfexamp...'>,
             <Asset id='jake' url='//images.contentful.com/cfexamplea...'>]
        """

        if query is None:
            query = {}
        self._normalize_select(query)

        return self._get(
            self.environment_url('/assets'),
            query
        )

    def locales(self, query=None):
        """Fetches all Locales from the Environment (up to the set limit, can be modified in `query`).

        # TODO: fix url
        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets/assets-collection/get-all-assets-of-a-space

        :param query: (optional) Dict with API options.
        :return: List of :class:`Locale <contentful.locale.Locale>` objects.
        :rtype: List of contentful.locale.Locale

        Usage:
            >>> locales = client.locales()
            [<Locale[English (United States)] code='en-US' default=True fallback_code=None optional=False>]
        """

        if query is None:
            query = {}

        return self._get(
            self.environment_url('/locales'),
            query
        )

    def sync(self, query=None):
        """Fetches content from the Sync API.

        API Reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization/initial-synchronization/query-entries

        :param query: (optional) Dict with API options.
        :return: :class:`SyncPage <contentful.sync_page.SyncPage>` object.
        :rtype: contentful.sync_page.SyncPage

        Usage:
            >>> sync_page = client.sync({'initial': True})
            <SyncPage next_sync_token='w5ZGw6JFwqZmVcKsE8Kow4grw45QdybC...'>
        """

        if query is None:
            query = {}
        self._normalize_sync(query)

        return self._get(
            self.environment_url('/sync'),
            query
        )

    def environment_url(self, url):
        """Formats the URL with the environment."""

        return "/environments/{0}{1}".format(
            self.environment,
            url
        )

    def _normalize_select(self, query):
        """
        If the query contains the :select operator, we enforce :sys properties.
        The SDK requires sys.type to function properly, but as other of our
        SDKs require more parts of the :sys properties, we decided that every
        SDK should include the complete :sys block to provide consistency
        accross our SDKs.
        """

        if 'select' not in query:
            return

        if isinstance(
                query['select'],
                string_class()):
            query['select'] = [s.strip() for s in query['select'].split(',')]

        query['select'] = [s for s
                           in query['select']
                           if not s.startswith('sys.')]

        if 'sys' not in query['select']:
            query['select'].append('sys')

    def _normalize_sync(self, query):
        """
        Booleans are not properly serialized for GET params,
        therefore we enforce it to a truthy value.
        """

        if 'initial' in query:
            query['initial'] = 'true'

    def _validate_configuration(self):
        """
        Validates that required parameters are present.
        """

        if not self.space_id:
            raise ConfigurationException(
                'You will need to initialize a client with a Space ID'
            )
        if not self.access_token:
            raise ConfigurationException(
                'You will need to initialize a client with an Access Token'
            )
        if not self.api_url:
            raise ConfigurationException(
                'The client configuration needs to contain an API URL'
            )
        if not self.default_locale:
            raise ConfigurationException(
                'The client configuration needs to contain a Default Locale'
            )
        if not self.api_version or self.api_version < 1:
            raise ConfigurationException(
                'The API Version must be a positive number'
            )

    def _cache_content_types(self):
        """
        Updates the Content Type Cache.
        """

        ContentTypeCache.update_cache(self)

    def _contentful_user_agent(self):
        """
        Sets the X-Contentful-User-Agent header.
        """
        header = {}
        from . import __version__
        header['sdk'] = {
            'name': 'contentful.py',
            'version': __version__
        }
        header['app'] = {
            'name': self.application_name,
            'version': self.application_version
        }
        header['integration'] = {
            'name': self.integration_name,
            'version': self.integration_version
        }
        header['platform'] = {
            'name': 'python',
            'version': platform.python_version()
        }

        os_name = platform.system()
        if os_name == 'Darwin':
            os_name = 'macOS'
        elif not os_name or os_name == 'Java':
            os_name = None
        elif os_name and os_name not in ['macOS', 'Windows']:
            os_name = 'Linux'
        header['os'] = {
            'name': os_name,
            'version': platform.release()
        }

        def format_header(key, values):
            header = "{0} {1}".format(key, values['name'])
            if values['version'] is not None:
                header = "{0}/{1}".format(header, values['version'])
            return "{0};".format(header)

        result = []
        for k, values in header.items():
            if not values['name']:
                continue
            result.append(format_header(k, values))

        return ' '.join(result)

    def _request_headers(self):
        """
        Sets the default Request Headers.
        """

        headers = {
            'X-Contentful-User-Agent': self._contentful_user_agent(),
            'Content-Type': 'application/vnd.contentful.delivery.v{0}+json'.format(  # noqa: E501
                self.api_version
            )
        }

        if self.authorization_as_header:
            headers['Authorization'] = 'Bearer {0}'.format(self.access_token)

        headers['Accept-Encoding'] = 'gzip' if self.gzip_encoded else 'identity'

        return headers

    def _url(self, url):
        """
        Creates the Request URL.
        """

        protocol = 'https' if self.https else 'http'
        return '{0}://{1}/spaces/{2}{3}'.format(
            protocol,
            self.api_url,
            self.space_id,
            url
        )

    def _normalize_query(self, query):
        """
        Converts Arrays in the query to comma
        separaters lists for proper API handling.
        """

        for k, v in query.items():
            if isinstance(v, list):
                query[k] = ','.join([str(e) for e in v])

    def _http_get(self, url, query):
        """
        Performs the HTTP GET Request.
        """

        if not self.authorization_as_header:
            query.update({'access_token': self.access_token})

        response = None

        self._normalize_query(query)

        kwargs = {
            'params': query,
            'headers': self._request_headers(),
            'timeout': self.timeout_s
        }

        if self._has_proxy():
            kwargs['proxies'] = self._proxy_parameters()

        response = requests.get(
            self._url(url),
            **kwargs
        )

        if response.status_code == 429:
            raise RateLimitExceededError(response)

        return response

    def _get(self, url, query=None):
        """
        Wrapper for the HTTP Request,
        Rate Limit Backoff is handled here,
        Responses are Processed with ResourceBuilder.
        """

        if query is None:
            query = {}

        response = retry_request(self)(self._http_get)(url, query=query)

        if self.raw_mode:
            return response

        if response.status_code != 200:
            error = get_error(response)
            if self.raise_errors:
                raise error
            return error

        localized = query.get('locale', '') == '*'
        return ResourceBuilder(
            self.default_locale,
            localized,
            response.json(),
            max_depth=self.max_include_resolution_depth,
            reuse_entries=self.reuse_entries
        ).build()

    def _has_proxy(self):
        """
        Checks if a Proxy was set.
        """

        return self.proxy_host

    def _proxy_parameters(self):
        """
        Builds Proxy parameters Dict from
        client options.
        """

        proxy_protocol = ''
        if self.proxy_host.startswith('https'):
            proxy_protocol = 'https'
        else:
            proxy_protocol = 'http'

        proxy = '{0}://'.format(proxy_protocol)
        if self.proxy_username and self.proxy_password:
            proxy += '{0}:{1}@'.format(self.proxy_username, self.proxy_password)

        proxy += sub(r'https?(://)?', '', self.proxy_host)

        if self.proxy_port:
            proxy += ':{0}'.format(self.proxy_port)

        return {
            'http': proxy,
            'https': proxy
        }

    def __repr__(self):
        return '<contentful.Client space_id="{0}" access_token="{1}" default_locale="{2}">'.format(  # noqa: E501
            self.space_id,
            self.access_token,
            self.default_locale
        )
import json
from unittest import TestCase
from contentful.errors import (
    HTTPError,
    NotFoundError,
    BadRequestError,
    AccessDeniedError,
    UnauthorizedError,
    RateLimitExceededError,
    ServerError,
    ServiceUnavailableError,
    get_error
)
from contentful.utils import retry_request, json_error_class
from contentful.client import Client


class ErrorsTest(TestCase):
    def test_default_additional_info_is_empty(self):
        response = MockResponse(512, "not json", invalid_json=True)
        error = get_error(response)

        self.assertEqual(error._additional_error_info(), [])

    def test_default_error_message(self):
        response = MockResponse(512, "not json", invalid_json=True)
        error = get_error(response)

        expected_error = "\n".join([
            "HTTP status code: 512",
            "Message: The following error was received: not json"
        ])
        self.assertEqual(str(error), expected_error)

    def test_generic_details(self):
        response = MockResponse(512, {
            'details': 'some text'
        })
        error = get_error(response)

        expected_error = "\n".join([
            "HTTP status code: 512",
            "Message: The following error was received: {'details': 'some text'}",
            "Details: some text"
        ])
        self.assertEqual(str(error), expected_error)

    def test_not_found_error(self):
        response = MockResponse(404, {
            'message': 'The resource could not be found.',
            'details': {
              'type': 'Asset',
              'id': 'foobar'
            },
            'requestId': '$foobar123'
        })

        error = get_error(response)

        self.assertEqual(error.status_code, 404)
        expected_error = "\n".join([
            "HTTP status code: 404",
            "Message: The resource could not be found.",
            "Details: The requested Asset could not be found. ID: foobar.",
            "Request ID: $foobar123"
        ])
        self.assertEqual(str(error), expected_error)
        self.assertTrue(isinstance(error, NotFoundError))

    def test_not_found_error_with_sys_on_details(self):
        response = MockResponse(404, {
            'message': 'The resource could not be found.',
            'details': {
              'sys': {
                'type': 'Space',
                'id': 'foobar'
              }
            },
            'requestId': '$foobar123'
        })

        error = get_error(response)

        self.assertEqual(error.status_code, 404)
        expected_error = "\n".join([
            "HTTP status code: 404",
            "Message: The resource could not be found.",
            "Details: The requested Space could not be found. ID: foobar.",
            "Request ID: $foobar123"
        ])
        self.assertEqual(str(error), expected_error)
        self.assertTrue(isinstance(error, NotFoundError))

    def test_not_found_error_details_is_a_string(self):
        response = MockResponse(404, {
            'message': 'The resource could not be found.',
            'details': 'The resource could not be found',
            'requestId': '$foobar123'
        })

        error = get_error(response)

        self.assertEqual(error.status_code, 404)
        expected_error = "\n".join([
            "HTTP status code: 404",
            "Message: The resource could not be found.",
            "Details: The resource could not be found",
            "Request ID: $foobar123"
        ])
        self.assertEqual(str(error), expected_error)
        self.assertTrue(isinstance(error, NotFoundError))

    def test_bad_request_error(self):
        response = MockResponse(400, {
            'message': 'The query you sent was invalid. Probably a filter or ordering specification is not applicable to the type of a field.',
            'details': {
                'errors': [
                    {
                        'details': 'The path "invalid_param" is not recognized'
                    }
                ]
            },
            'requestId': '$foobar234'
        })

        error = get_error(response)

        self.assertEqual(error.status_code, 400)
        expected_error = "\n".join([
            "HTTP status code: 400",
            "Message: The query you sent was invalid. Probably a filter or ordering specification is not applicable to the type of a field.",
            'Details: The path "invalid_param" is not recognized',
            "Request ID: $foobar234"
        ])
        self.assertEqual(str(error), expected_error)

        self.assertTrue(isinstance(error, BadRequestError))