Returns the results of an API call.

            This is the main work horse of the class. It builds the API query
            string and sends the request to MetaSmoke. If there are multiple
            pages of results, and we've configured `max_pages` to be greater than
            1, it will automatically paginate through the results and return a
            single object.

            Returned data will appear in the `items` key of the resulting
            dictionary.

            :param endpoint: (string) The API end point being called. Available endpoints are listed on
                the official `API Documentation <https://github.com/Charcoal-SE/metasmoke/wiki/API-Documentation>`__.

                This can be as simple as ``fetch('posts/feedback')``, to call feedback end point

                If calling an end point that takes additional parameter, such as `id`s
                pass the ids as a list to the `ids` key:

                    .. code-block:: python

                        fetch('posts/{ids}', ids=[1,2,3])

                This will attempt to retrieve the posts for the three listed ids.

                If no end point is passed, a ``ValueError`` will be raised
            :param page: (int) The page in the results to start at. By default, it will start on
                the first page and automatically paginate until the result set
                reaches ``max_pages``.
            :param kwargs: Parameters accepted by individual endpoints. These parameters
                **must** be named the same as described in the endpoint documentation
            :rtype: (dictionary) A dictionary containing wrapper data regarding the API call
                and the results of the call in the `items` key. If multiple
                pages were received, all of the results will appear in the
                ``items`` tag.

def fetch(self, endpoint=None, page=1, **kwargs):
        """Returns the results of an API call.

            This is the main work horse of the class. It builds the API query
            string and sends the request to MetaSmoke. If there are multiple
            pages of results, and we've configured `max_pages` to be greater than
            1, it will automatically paginate through the results and return a
            single object.

            Returned data will appear in the `items` key of the resulting
            dictionary.

            :param endpoint: (string) The API end point being called. Available endpoints are listed on
                the official `API Documentation <https://github.com/Charcoal-SE/metasmoke/wiki/API-Documentation>`__.

                This can be as simple as ``fetch('posts/feedback')``, to call feedback end point

                If calling an end point that takes additional parameter, such as `id`s
                pass the ids as a list to the `ids` key:

                    .. code-block:: python

                        fetch('posts/{ids}', ids=[1,2,3])

                This will attempt to retrieve the posts for the three listed ids.

                If no end point is passed, a ``ValueError`` will be raised
            :param page: (int) The page in the results to start at. By default, it will start on
                the first page and automatically paginate until the result set
                reaches ``max_pages``.
            :param kwargs: Parameters accepted by individual endpoints. These parameters
                **must** be named the same as described in the endpoint documentation
            :rtype: (dictionary) A dictionary containing wrapper data regarding the API call
                and the results of the call in the `items` key. If multiple
                pages were received, all of the results will appear in the
                ``items`` tag.
            """

        if not endpoint:
            raise ValueError('No endpoint provided.')

        self._endpoint = endpoint

        params = {"per_page": self.per_page,
                  "page": page,
                  "key": self._api_key
                  }

        # This block will replace {ids} placeholds in end points
        # converting .fetch('posts/{ids}', ids=[222, 1306, 99999]) to
        #   posts/222;1306;99999
        for k, value in kwargs.items():
            if "{" + k + "}" in endpoint:
                endpoint = endpoint.replace("{" + k + "}", ';'.join(str(x) for x in value))
                kwargs.pop(k, None)

        date_time_keys = ['from_date', 'to_date']
        for k in date_time_keys:
            if k in kwargs:
                if isinstance(kwargs[k], datetime.datetime):
                    kwargs[k] = int(calendar.timegm(kwargs[k].utctimetuple()))

        # This block will see if there there are ids remaining
        # This would occur if the developer passed `posts` instead of `posts/{ids}` to `fetch`
        # If this is the case, then convert to a string and assume this goes at the end of the endpoint

        if 'ids' in kwargs:
            ids = ';'.join(str(x) for x in kwargs['ids'])
            kwargs.pop('ids', None)
            endpoint += "/{}".format(ids)

        params.update(kwargs)

        data = []
        run_cnt = 1
        while run_cnt <= self.max_pages:
            run_cnt += 1

            base_url = "{}{}/".format(self._base_url, endpoint)

            try:
                response = requests.get(base_url, params=params, proxies=self.proxy)
            except requests.exceptions.ConnectionError as e:
                raise SmokeAPIError(self._previous_call, str(e), str(e), str(e))

            self._previous_call = response.url

            try:
                response.encoding = 'utf-8-sig'
                response = response.json()
            except ValueError as e:
                raise SmokeAPIError(self._previous_call, str(e), str(e), str(e))

            try:
                code = response["error_code"]
                name = response["error_name"]
                message = response["error_message"]
                raise SmokeAPIError(self._previous_call, code, name, message)
            except KeyError:
                pass  # This means there is no error

            data.append(response)

            if len(data) < 1:
                break

            if 'has_more' in response and response['has_more']:
                params["page"] += 1
            else:
                break

        r = []
        for d in data:
            r.extend(d['items'])
        items = list(chain(r))
        has_more = data[-1]['has_more'] if 'has_more' in data[-1] else False
        result = {'has_more': has_more,
                  'page': params['page'],
                  'total': len(items),
                  'items': items}

        return result