Get a URL via the cache.

        If the URL exists in the cache, return the cached value. Otherwise perform the request,
        store the resulting content in the cache and return it.

        Throws a :class:`RuntimeError` if the request results in an error.

        Args:
            url (str): URL to request
            store_on_error (bool): If True, store request results in cache even if request results in an
                an error. Otherwise (default) do not store results when an error occurs. Cached content
                equals exception message.
            xpath (str): If given (default is None), parses the response content to html, searches the first
                node matching the given xpath and returns only that node (as UTF8-encoded html). Also, only
                stores this node's html in the cache. Raises a ``RuntimeError`` if the xpath cannot be found
                in the response.
            rate_limit (float): If not None (default), wait at least this many seconds between the previous
                request and the current one (this does not apply to cache hits).
            log_hits (bool): If True, log cache hits
            log_misses (bool): If True, log cache misses

        Returns:
            str: request content

def get(self, url, store_on_error=False, xpath=None, rate_limit=None, log_hits=True, log_misses=True):
        """Get a URL via the cache.

        If the URL exists in the cache, return the cached value. Otherwise perform the request,
        store the resulting content in the cache and return it.

        Throws a :class:`RuntimeError` if the request results in an error.

        Args:
            url (str): URL to request
            store_on_error (bool): If True, store request results in cache even if request results in an
                an error. Otherwise (default) do not store results when an error occurs. Cached content
                equals exception message.
            xpath (str): If given (default is None), parses the response content to html, searches the first
                node matching the given xpath and returns only that node (as UTF8-encoded html). Also, only
                stores this node's html in the cache. Raises a ``RuntimeError`` if the xpath cannot be found
                in the response.
            rate_limit (float): If not None (default), wait at least this many seconds between the previous
                request and the current one (this does not apply to cache hits).
            log_hits (bool): If True, log cache hits
            log_misses (bool): If True, log cache misses

        Returns:
            str: request content
        """

        try:
            # get cached request - if none is found, this throws a NoResultFound exception
            cached = self._query(url, xpath).one()
            if log_hits:
                config.logger.info("Request cache hit: " + url)

            # if the cached value is from a request that resulted in an error, throw an exception
            if cached.status_code != requests.codes.ok:
                raise RuntimeError("Cached request returned an error, code " + str(cached.status_code))
        except NoResultFound:
            if log_misses:
                config.logger.info("Request cache miss: " + url)

            # perform the request
            try:

                # rate limit
                if rate_limit is not None and self.last_query is not None:
                    to_sleep = rate_limit - (datetime.datetime.now() - self.last_query).total_seconds()
                    if to_sleep > 0:
                        time.sleep(to_sleep)

                self.last_query = datetime.datetime.now()

                response = requests.get(url)
                status_code = response.status_code
                # get 'text', not 'content', because then we are sure to get unicode
                content = response.text
                response.close()

                if xpath is not None:
                    doc = html.fromstring(content)
                    nodes = doc.xpath(xpath)
                    if len(nodes) == 0:
                        # xpath not found; set content and status code, exception is raised below
                        content = "xpath not found: " + xpath
                        status_code = ERROR_XPATH_NOT_FOUND
                    else:
                        # extract desired node only
                        content = html.tostring(nodes[0], encoding='unicode')

            except requests.ConnectionError as e:
                # on a connection error, write exception information to a response object
                status_code = ERROR_CONNECTION_ERROR
                content = str(e)

            # a new request cache object
            cached = CachedRequest(url=str(url),
                                   content=content,
                                   status_code=status_code,
                                   xpath=xpath,
                                   queried_on=datetime.datetime.now())

            # if desired, store the response even if an error occurred
            if status_code == requests.codes.ok or store_on_error:
                self.session.add(cached)
                self.session.commit()

            if status_code != requests.codes.ok:
                raise RuntimeError("Error processing the request, " + str(status_code) + ": " + content)

        return cached.content