docs: def fetch(self, endpoint=None, page=1, key=None, filter='default', **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 Stack Exchange. 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: http://api.stackexchange.com/docs
  
              This can be as simple as ``fetch('answers')``, to call the answers
              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('answers/{}', ids=[1,2,3])
  
              This will attempt to retrieve the answers 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
              reached ``max_pages``.
          :param key: (string) The site you are issuing queries to.
          :param filter: (string) The filter to utilize when calling an endpoint. Different filters
              will return different keys. The default is ``default`` and this will
              still vary depending on what the API returns as default for a
              particular endpoint
          :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 end point provided.')
  
          self._endpoint = endpoint
  
          params = {
              "pagesize": self.page_size,
              "page": page,
              "filter": filter
          }
  
          if self.key:
              params['key'] = self.key
          if self.access_token:
              params['access_token'] = self.access_token
  
          # This block will replace {ids} placeholds in end points
          # converting .fetch('badges/{ids}', ids=[222, 1306, 99999]) to
          #   badges/222;1306;99999
          for k, value in list(kwargs.items()):
              if "{" + k + "}" in endpoint:
                  endpoint = endpoint.replace("{"+k+"}", ';'.join(requests.compat.quote_plus(str(x)) for x in value))
                  kwargs.pop(k, None)
  
          date_time_keys = ['fromdate', 'todate', 'since', 'min', 'max']
          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 `badges` instead of `badges/{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)
          if self._api_key:
              params['site'] = self._api_key
  
          data = []
          run_cnt = 1
          backoff = 0
          total = 0
          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 StackAPIError(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 StackAPIError(self._previous_call, str(e), str(e), str(e))
  
              try:
                  error = response["error_id"]
                  code = response["error_name"]
                  message = response["error_message"]
                  raise StackAPIError(self._previous_call, error, code, message)
              except KeyError:
                  pass  # This means there is no error
  
              if key:
                  data.append(response[key])
              else:
                  data.append(response)
  
              if len(data) < 1:
                  break
  
              backoff = 0
              total = 0
              page = 1
              if 'backoff' in response:
                  backoff = int(response['backoff'])
                  sleep(backoff+1)        # Sleep an extra second to ensure no timing issues
              if 'total' in response:
                  total = response['total']
              if 'has_more' in response and response['has_more'] and run_cnt <= self.max_pages:
                  params["page"] += 1
              else:
                  break
  
          r = []
          for d in data:
              if 'items' in d:
                  r.extend(d['items'])
          result = {'backoff': backoff,
                    'has_more': False if 'has_more' not in data[-1] else data[-1]['has_more'],
                    'page': params['page'],
                    'quota_max': -1 if 'quota_max' not in data[-1] else data[-1]['quota_max'],
                    'quota_remaining': -1 if 'quota_remaining' not in data[-1] else data[-1]['quota_max'],
                    'total': total,
                    'items': list(chain(r))}
  
          return result