Repository: DOV-Vlaanderen/pydov

Issue:
caching feature
Using the package, XML files need to be downloaded by the user. As we should not expect that users will be fully aware of when XML downloads are needed (versus pure WFS requests) we can counteract multiple downloads by providing a caching functionality:

- XML files stored as files in cache folder
- before requesting an XML file, check the cache folder for existing local XML files . 
- check for age; if older than X weeks, redownload the file

(is a package wide functionality, used by the different modules, basically a wrapper around `pydov.types.abstract.AbstractDovType#_get_xml_data`

Discussion:

Patch:
diff --git a/docs/caching.rst b/docs/caching.rst
new file mode 100644
index 0000000..8cf820b
--- /dev/null
+++ b/docs/caching.rst
@@ -0,0 +1,112 @@
+=======
+Caching
+=======
+
+To speed up subsequent queries involving similar data, pydov uses a caching
+mechanism where raw DOV XML data is cached locally for later reuse.
+
+By default, this is a global cache shared by all usages of pydov on the same
+system. This means subsequent calls in the same script, multiple runs of
+the same script over time and multiple implementations or applications
+using pydov on the same system all use the same cache.
+
+The default cache will reuse cached data for up to two weeks: if cached data
+for an object is available and has been downloaded less than two weeks ago,
+it will be reused in favor of downloading from the DOV services.
+
+As mentioned below some convenient utility methods are provided to handle
+disk usage. However, pydov does not change any files present on disk. This
+holds true for all files, also those in the cache directory. It is up to the
+user to keep track of disk usage etc.
+
+Disabling the cache
+*******************
+You can (temporarily!) disable the caching mechanism by issuing::
+
+    import pydov
+
+    pydov.cache = None
+
+This disables both the saving of newly downloaded data in the cache, as well
+as reusing existing data in the cache. It remains valid for the time being of
+the instantiated pydov.cache object.
+It does not delete existing data in the cache.
+
+Changing the location of cached data
+************************************
+
+By default, pydov stores the cache in a temporary directory provided by the
+user's operating system. On Windows, the cache is usually located in::
+
+    C:\Users\username\AppData\Local\Temp\pydov\
+
+If you want the cached xml files to be saved in another location you can define
+your own cache, as follows::
+
+    import pydov.util.caching
+
+    pydov.cache = pydov.util.caching.TransparentCache(
+        cachedir=r'C:\temp\pydov'
+    )
+
+Besides controlling the cache's location, this also allows using a different
+cache in different scripts or projects.
+
+Mind that xmls are stored by search type because permalinks are not unique
+across types. Therefore, the dir structure of the cache will look like, e.g.::
+
+    ...\pydov\boring\filename.xml
+    ...\pydov\filter\filename.xml
+
+
+Changing the maximum age of cached data
+***************************************
+
+If you work with rapidly changing data or want to control when cached data
+is renewed, you can do so by changing the maximum age of cached data to
+be considered valid for the current runtime::
+
+    import pydov.util.caching
+    import datetime
+
+    pydov.cache = pydov.util.caching.TransparentCache(
+        max_age=datetime.timedelta(days=1)
+    )
+
+If a cached version exists and is younger than the maximum age, it is used
+in favor of renewing the data from DOV services. If no cached version
+exists or is older than the maximum age, the data is renewed and saved
+in the cache.
+
+Note that data older than the maximum age is not automatically deleted from
+the cache.
+
+Cleaning the cache
+******************
+
+During normal use the cache only grows by adding new objects and overwriting
+existing ones with a new version. Should you want clean the cache of old
+items or remove the cache entirely, you can do so manually by calling the
+respective functions.
+
+To clean the cache, removing all records older than the maximum age, you can
+issue::
+
+    import pydov
+
+    pydov.cache.clean()
+
+
+Since we use a temporary directory provided by the operating system, we rely
+on the operating system to clean the folder when it deems necessary.
+
+Should you want to remove the pydov cache from code yourself, you can do so
+by issuing::
+
+    import pydov
+
+    pydov.cache.remove()
+
+
+This will erase the entire cache, not only the records older than the
+maximum age.
diff --git a/docs/description_output_dataframes.rst b/docs/description_output_dataframes.rst
index 280b06d..d49c10e 100644
--- a/docs/description_output_dataframes.rst
+++ b/docs/description_output_dataframes.rst
@@ -1,3 +1,7 @@
+============
+Object types
+============
+
 Interpretations
 ===============
 
diff --git a/docs/discussion_note_boringen_methods.rst b/docs/discussion_note_boringen_methods.rst
index 0010d15..9bd43ae 100644
--- a/docs/discussion_note_boringen_methods.rst
+++ b/docs/discussion_note_boringen_methods.rst
@@ -1,3 +1,4 @@
+===================================================
 Classes and methods for boringen and interpetations
 ===================================================
 
@@ -6,7 +7,7 @@ Possible schema:
 .. code-block:: python
 
    import pandas
-   
+
    class DovSearch(object)
        def __init__(self, ):
            """instantiate class for certain location
@@ -28,26 +29,26 @@ Possible schema:
                                   # if not required
            # different steps to come to dataframe
            return dataframe_with_columns_of_interest
-   
+
    class DovGrondwaterFilter(DovSearch):
        def __init__(self, ):
            """instantiate class for certain location
-           
+
            """
-           
+
            pass
 
        def get_data(location=None, query=None, columns=None, extra_argument=None):
            """for the filters one can add an additional argument to get 'observaties' or
            'kwaliteitsdata', joined with the location which is returned by default
-           """ 
+           """
            pass
 
    class DovBoringen(DovSearch):
        def __init__(self, ):
-           """instantiate class 
+           """instantiate class
            """
-           
+
            pass
 
        def list_interpretations(self, ):
@@ -57,12 +58,12 @@ Possible schema:
 
        def get_interpretation(self, interpretation):
            """get data from wfs and/or xml for a certain interpretation
-           
+
            Parameters
            ----------
            interpretation: string
                 the selected intepretation
-           
+
            """
            self.ip = globals()[interpretation]()
            df_boring .... get data from....
@@ -76,7 +77,7 @@ Possible schema:
        """class for interpretation related stuff
        """
        def __init__(self,):
-           """instantiate class 
+           """instantiate class
            """
            self.defined_interpretations = ['InformeleStratigrafie',
                                            'FormeleStratigrafie',
@@ -93,24 +94,24 @@ Possible schema:
        """
        def __init__(self, location=None):
            """instantiate class for certain location
-           
+
            location can be anything from coordinates (with buffer), bbox
            or polygon, default None
            """
            if location:
                self.location = location # add method to derive location from input
-           self.headers = ['pkey_interpretatie', 
-                           'pkey_boring', 
-                           'pkey_sondering', 
+           self.headers = ['pkey_interpretatie',
+                           'pkey_boring',
+                           'pkey_sondering',
                            'diepte_laag_van',
                            'diepte_laag_tot',
                            'aquifer']
-           
+
        def get_dataframe(self, input):
            """create dataframe from input
-           
+
            """
-           self.df = pd.DataFrame(input, columns=self.headers) 
+           self.df = pd.DataFrame(input, columns=self.headers)
 
    """
    Examples
@@ -126,5 +127,4 @@ Possible schema:
    >>> intepretatie = HydrogeologischeStratigrafie()
    >>> interpretatie_metadata = interpretatie.get_metadata()
    >>> df_interpetatie = intepretatie.get_data_interpretatie(location, query, columns=[columns of interest])
-   """   
-   
\ No newline at end of file
+   """
diff --git a/docs/notebooks/example_caching.ipynb b/docs/notebooks/example_caching.ipynb
new file mode 100644
index 0000000..17d7c10
--- /dev/null
+++ b/docs/notebooks/example_caching.ipynb
@@ -0,0 +1,589 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Example of XML caching for pydov"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Introduction"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "To speed up subsequent queries involving similar data, pydov uses a caching mechanism where raw DOV XML data is cached locally for later reuse. For regular usage of the package and data requests, the cache will be a *convenient* feature speeding up the time for subsequent queries. However, in case you want to alter the configuration or cache handling, this notebook illustrates some use cases on the cache handling."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Use cases:\n",
+    "* Check cached files\n",
+    "* Speed up subsequent queries\n",
+    "* Disabling the cache\n",
+    "* Changing the location of cached data\n",
+    "* Changing the maximum age of cached data\n",
+    "* Cleaning the cache"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# check pydov path\n",
+    "import warnings; warnings.simplefilter('ignore')\n",
+    "import pydov"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Use cases"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Check cached files"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "from pydov.search.boring import BoringSearch\n",
+    "boring = BoringSearch()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The `pydov.cache.cachedir` defines the directory on the file system used to cache DOV files:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "/tmp/pydov\n",
+      "directories:  []\n"
+     ]
+    }
+   ],
+   "source": [
+    "# check the cache dir\n",
+    "import os\n",
+    "import pydov.util.caching\n",
+    "cachedir = pydov.cache.cachedir\n",
+    "print(cachedir)\n",
+    "print('directories: ', os.listdir(cachedir))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Speed up subsequent queries"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "To illustrate the convenience of the caching during subsequent data requests, consider the following request, while measuring the time:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "CPU times: user 2.6 s, sys: 136 ms, total: 2.74 s\n",
+      "Wall time: 35.1 s\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Get all borehole data in a bounding box (llx, llxy, ulx, uly) and timeit\n",
+    "%time df = boring.search(location=(150145, 205030, 155150, 206935))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "number of files:  107\n",
+      "files present:  ['1973-018152.xml', '1970-061364.xml', '1879-122256.xml', '1996-081802.xml', '1973-104728.xml', '1973-060207.xml', '1895-121248.xml', '1986-005594.xml', '1953-121362.xml', '1879-119364.xml', '2018-153957.xml', '1970-061447.xml', '2018-156632.xml', '1973-104727.xml', '1973-081811.xml', '2018-156634.xml', '1938-121359.xml', '1895-121242.xml', '1894-122153.xml', '1879-121387.xml', '1974-010351.xml', '1969-033207.xml', '1879-121401.xml', '1970-061363.xml', '1970-061446.xml', '1936-122224.xml', '1970-061450.xml', '1879-121293.xml', '1895-121247.xml', '1970-061362.xml', '1973-060208.xml', '1986-059815.xml', '1986-059816.xml', '1969-033208.xml', '1976-015780.xml', '1879-121292.xml', '1879-121424.xml', '1984-081834.xml', '1970-061366.xml', '1970-104899.xml', '1986-059814.xml', '1969-092689.xml', '1970-061365.xml', '1970-061444.xml', '1894-122154.xml', '1969-033217.xml', '2017-148854.xml', '1969-033211.xml', '1953-121361.xml', '1969-033209.xml', '1976-015779.xml', '2017-153161.xml', '1953-121327.xml', '2018-155580.xml', '1895-121232.xml', '1975-010345.xml', '2017-152011.xml', '1969-033215.xml', '1976-015782.xml', '1923-121200.xml', '1970-018757.xml', '1970-104897.xml', '1969-033214.xml', '1969-092685.xml', '1970-061445.xml', '1923-121199.xml', '1987-119382.xml', '1986-005597.xml', '1969-033213.xml', '1976-015298.xml', '1879-121412.xml', '1969-033218.xml', '1970-018762.xml', '1984-081833.xml', '1976-015297.xml', '1970-018763.xml', '1894-121258.xml', '2018-154057.xml', '1976-015781.xml', '1895-121241.xml', '1986-005596.xml', '1969-033212.xml', '1894-122155.xml', '1996-021717.xml', '1970-061443.xml', '1986-005598.xml', '1970-104898.xml', '2018-156633.xml', '1969-033220.xml', '1895-121244.xml', '1932-121315.xml', '1969-092688.xml', '2018-155266.xml', '1969-092686.xml', '1978-012352.xml', '1985-084552.xml', '1969-033206.xml', '1970-061442.xml', '1969-033216.xml', '1969-092687.xml', '1970-104900.xml', '1970-061454.xml', '1938-121360.xml', '1973-104723.xml', '1978-121458.xml', '1969-033219.xml', '1976-014856.xml']\n"
+     ]
+    }
+   ],
+   "source": [
+    "# The structure of cachedir implies a seperate directory for each query type, since permalinks are not unique accross types\n",
+    "# In this example 'boring' will be queried, terefore list xmls in the cache of the 'boring' type\n",
+    "# list files present\n",
+    "print('number of files: ', len(os.listdir(os.path.join(pydov.cache.cachedir, 'boring'))))\n",
+    "print('files present: ', os.listdir(os.path.join(pydov.cache.cachedir, 'boring')))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Rerun the previous request and timeit again:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "CPU times: user 16 ms, sys: 4 ms, total: 20 ms\n",
+      "Wall time: 243 ms\n"
+     ]
+    }
+   ],
+   "source": [
+    "%time df = boring.search(location=(153145, 206930, 153150, 206935))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The use of the cache decreased the runtime by a factor 100 in the current example. This will increase drastically if more permalinks are queried since the download takes much longer than the IO at runtime."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Disabling the cache"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can (temporarily!) disable the caching mechanism. This disables both the saving of newly downloaded data in the cache, \n",
+    "as well as reusing existing data in the cache. It remains valid for the time being of the instantiated pydov.cache object.\n",
+    "It does not delete existing data in the cache."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "number of files:  107\n"
+     ]
+    }
+   ],
+   "source": [
+    "# list number of files\n",
+    "print('number of files: ', len(os.listdir(os.path.join(cachedir, 'boring'))))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "                                         pkey_boring     boornummer         x  \\\n",
+      "0  https://www.dov.vlaanderen.be/data/boring/1895...   kb15d43w-B47  151600.0   \n",
+      "1  https://www.dov.vlaanderen.be/data/boring/1984...  kb15d43w-B403  151041.0   \n",
+      "\n",
+      "          y  mv_mtaw  start_boring_mtaw   gemeente  diepte_boring_van  \\\n",
+      "0  205998.0    15.00              15.00  Antwerpen                0.0   \n",
+      "1  205933.0    21.07              21.07  Antwerpen                0.0   \n",
+      "\n",
+      "   diepte_boring_tot datum_aanvang                                uitvoerder  \\\n",
+      "0                3.3    1895-01-04                                  onbekend   \n",
+      "1                7.0    1984-09-26  Universiteit Gent - Geologisch Instituut   \n",
+      "\n",
+      "   boorgatmeting  diepte_methode_van  diepte_methode_tot   boormethode  \n",
+      "0          False                 0.0                 3.3      onbekend  \n",
+      "1          False                 0.0                 7.0  droge boring  \n"
+     ]
+    }
+   ],
+   "source": [
+    "# disable caching\n",
+    "cache_orig = pydov.cache\n",
+    "pydov.cache = None\n",
+    "# new query\n",
+    "df = boring.search(location=(151000, 205930, 153000, 206000))\n",
+    "print(df.head())"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "number of files:  107\n"
+     ]
+    }
+   ],
+   "source": [
+    "# list number of files\n",
+    "print('number of files: ', len(os.listdir(os.path.join(cachedir, 'boring'))))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hence, no new files were added to the cache when disabling it."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The caching is disabled by removing the pydov.cache object from the namespace. If you want to enable caching again you must instantiate it anew."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "pydov.cache = cache_orig"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Changing the location of cached data"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "By default, pydov stores the cache in a temporary directory provided by the user's operating system. On Windows, the cache is usually located in:  `C:\\Users\\username\\AppData\\Local\\Temp\\pydov\\`\n",
+    "If you want the cached xml files to be saved in another location you can define your own cache for the current runtime. Mind that this does not change the location of previously saved data. No lookup in the old datafolder will be performed after changing the directory's location.\n",
+    "Besides controlling the cache's location, this also allows using different scripts or projects."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "import pydov.util.caching\n",
+    "\n",
+    "pydov.cache = pydov.util.caching.TransparentCache(\n",
+    "    cachedir=r'C:\\temp\\pydov'\n",
+    "    )"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "C:\\temp\\pydov\n"
+     ]
+    }
+   ],
+   "source": [
+    "cachedir = pydov.cache.cachedir\n",
+    "print(cachedir)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# for the sake of the example, change dir location back \n",
+    "pydov.cache = cache_orig\n",
+    "cachedir = pydov.cache.cachedir"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Changing the maximum age of cached data"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "If you work with rapidly changing data or want to control when cached data is renewed, you can do so by changing the maximum age of cached data to be considered valid for the currenct runtime. You can use 'weeks', 'days' or any other common datetime format.\n",
+    "If a cached version exists and is younger than the maximum age, it is used in favor of renewing the data from DOV services. If no cached version exists or is older than the maximum age, the data is renewed and saved in the cache.\n",
+    "Note that data older than the maximum age is not automatically deleted from the cache."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "0:00:01\n"
+     ]
+    }
+   ],
+   "source": [
+    "import pydov.util.caching\n",
+    "import datetime\n",
+    "pydov.cache = pydov.util.caching.TransparentCache(\n",
+    "    max_age=datetime.timedelta(seconds=1)\n",
+    "    )\n",
+    "print(pydov.cache.max_age)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "1973-018152.xml\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "'Fri Aug 31 10:53:06 2018'"
+      ]
+     },
+     "execution_count": 15,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from time import ctime\n",
+    "print(os.listdir(os.path.join(cachedir, 'boring'))[0])\n",
+    "ctime(os.path.getmtime(os.path.join(os.path.join(cachedir, 'boring'),\n",
+    "                                    os.listdir(os.path.join(cachedir, 'boring'))[0]\n",
+    "                                   )\n",
+    "                      )\n",
+    "     )"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "CPU times: user 2.17 s, sys: 152 ms, total: 2.32 s\n",
+      "Wall time: 34.4 s\n"
+     ]
+    }
+   ],
+   "source": [
+    "# rerun previous query \n",
+    "%time df = boring.search(location=(150145, 205030, 155150, 206935))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "1973-018152.xml\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "'Fri Aug 31 10:53:39 2018'"
+      ]
+     },
+     "execution_count": 17,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from time import ctime\n",
+    "print(os.listdir(os.path.join(cachedir, 'boring'))[0])\n",
+    "ctime(os.path.getmtime(os.path.join(os.path.join(cachedir, 'boring'),\n",
+    "                                    os.listdir(os.path.join(cachedir, 'boring'))[0]\n",
+    "                                   )\n",
+    "                      )\n",
+    "     )"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Cleaning the cache"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Since we use a temporary directory provided by the operating system, we rely on the operating system to clean the folder when it deems necessary."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "To clean the cache, removing all records older than the maximum age"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from time import sleep"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "number of files before clean:  107\n",
+      "number of files after clean:  0\n"
+     ]
+    }
+   ],
+   "source": [
+    "print('number of files before clean: ', len(os.listdir(os.path.join(cachedir, 'boring'))))\n",
+    "sleep(2) # remember we've put the caching age on 1 second\n",
+    "pydov.cache.clean()\n",
+    "print('number of files after clean: ', len(os.listdir(os.path.join(cachedir, 'boring'))))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Should you want to remove the pydov cache from code yourself, you can do so as illustrated below. Note that this will erase the entire cache, not only the records older than the maximum age:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "pydov.cache.remove()\n",
+    "# check existence of the cache directory:\n",
+    "print(os.path.exists(os.path.join(cachedir, 'boring')))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python [default]",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.5.5"
+  },
+  "nbsphinx": {
+   "execute": "never"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/reference.rst b/docs/reference.rst
index bdbac55..2b2d24d 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -4,7 +4,11 @@ API reference
 Searching
 ---------
 
-.. automodule:: pydov.search
+.. automodule:: pydov.search.abstract
+    :members:
+
+
+.. automodule:: pydov.search.boring
     :members:
 
 
@@ -18,6 +22,13 @@ Object types
     :members:
 
 
+Caching
+-------
+
+.. automodule:: pydov.util.caching
+    :members:
+
+
 OWS utilities
 -------------
 
diff --git a/docs/tutorials.rst b/docs/tutorials.rst
index 42d0c6b..605f9d3 100644
--- a/docs/tutorials.rst
+++ b/docs/tutorials.rst
@@ -22,3 +22,4 @@ To run these interactively online without installation, use the following binder
    notebooks/example_search_boringen.ipynb
    notebooks/example_search_grondwaterfilters.ipynb
    notebooks/example_search_interpretaties.ipynb
+   notebooks/example_caching.ipynb
diff --git a/docs/usage.rst b/docs/usage.rst
index a4b27c5..64d64ba 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -6,8 +6,10 @@ To use PyDOV in a project::
 
     import pydov
 
+
+
 .. toctree::
-  :caption: Object types
+   :maxdepth: 2
+
+   caching
 
-  description_output_dataframes
-  discussion_note_boringen_methods
\ No newline at end of file
diff --git a/pydov/__init__.py b/pydov/__init__.py
index 7c9e2a7..c6e26e1 100644
--- a/pydov/__init__.py
+++ b/pydov/__init__.py
@@ -1,4 +1,7 @@
 # -*- coding: utf-8 -*-
+import pydov.util.caching
 
 __author__ = """DOV-Vlaanderen"""
 __version__ = '0.1.0'
+
+cache = pydov.util.caching.TransparentCache()
diff --git a/pydov/types/abstract.py b/pydov/types/abstract.py
index 0dcbcff..b6890d1 100644
--- a/pydov/types/abstract.py
+++ b/pydov/types/abstract.py
@@ -6,6 +6,7 @@ import types
 from collections import OrderedDict
 from distutils.util import strtobool
 
+import pydov
 import numpy as np
 
 from owslib.etree import etree
@@ -485,7 +486,10 @@ class AbstractDovType(AbstractCommon):
             The raw XML data of this DOV object as bytes.
 
         """
-        return openURL(self.pkey + '.xml').read()
+        if pydov.cache:
+            return pydov.cache.get(self.pkey + '.xml')
+        else:
+            return openURL(self.pkey + '.xml').read()
 
     def _parse_subtypes(self, xml):
         """Parse the subtypes with the given XML data.
diff --git a/pydov/util/caching.py b/pydov/util/caching.py
new file mode 100644
index 0000000..fce23dc
--- /dev/null
+++ b/pydov/util/caching.py
@@ -0,0 +1,223 @@
+# -*- coding: utf-8 -*-
+"""Module implementing a local cache for downloaded XML files."""
+import datetime
+import os
+import re
+import shutil
+import tempfile
+from io import open
+
+from owslib.util import openURL
+
+
+class TransparentCache(object):
+    """Class for transparent caching of downloaded XML files from DOV."""
+    def __init__(self, max_age=datetime.timedelta(weeks=2), cachedir=None):
+        """Initialisation.
+
+        Set up the instance variables and create the cache directory if
+        it does not exists already.
+
+        Parameters
+        ----------
+        max_age : datetime.timedelta, optional
+            The maximum age of a cached XML file to be valid. If the last
+            modification date of the file is before this time, it will be
+            redownloaded. Defaults to two weeks.
+        cachedir : str, optional
+            Path of the directory that will be used to save the cached XML
+            files. Be sure to use a directory that will only be used for
+            this PyDOV cache. Default to a temporary directory provided by
+            the operating system.
+
+        """
+        if cachedir:
+            self.cachedir = cachedir
+        else:
+            self.cachedir = os.path.join(tempfile.gettempdir(), 'pydov')
+        self.max_age = max_age
+
+        self._re_type_key = re.compile(
+            r'https?://www.dov.vlaanderen.be/data/([' '^/]+)/([^\.]+)')
+
+        try:
+            if not os.path.exists(self.cachedir):
+                os.makedirs(self.cachedir)
+        except Exception:
+            pass
+
+    def _get_type_key(self, url):
+        """Parse a DOV permalink and return the datatype and object key.
+
+        Parameters
+        ----------
+        url : str
+            Permanent URL to a DOV object.
+
+        Returns
+        -------
+        datatype : str
+            Datatype of the DOV object referred to by the URL.
+        key : str
+            Unique and permanent key of the instance of the DOV object
+            referred to by the URL.
+
+        """
+        datatype = self._re_type_key.search(url)
+        if datatype and len(datatype.groups()) > 1:
+            return datatype.group(1), datatype.group(2)
+
+    def _save(self, datatype, key, content):
+        """Save the given content in the cache.
+
+        Parameters
+        ----------
+        datatype : str
+            Datatype of the DOV object to save.
+        key : str
+            Unique and permanent object key of the DOV object to save.
+        content : : bytes
+            The raw XML data of this DOV object as bytes.
+
+        """
+        folder = os.path.join(self.cachedir, datatype)
+
+        if not os.path.exists(folder):
+            os.makedirs(folder)
+
+        filepath = os.path.join(folder, key + '.xml')
+        with open(filepath, 'w', encoding='utf-8') as f:
+            f.write(content.decode('utf-8'))
+
+    def _valid(self, datatype, key):
+        """Check if a valid version of the given DOV object exists in the
+        cache.
+
+        A cached version is valid if it exists and the last modification
+        time of the file is after the maximum age defined on initialisation.
+
+        Parameters
+        ----------
+        datatype : str
+            Datatype of the DOV object.
+        key : str
+            Unique and permanent object key of the DOV object.
+
+        Returns
+        -------
+        bool
+            True if a valid cached version exists, False otherwise.
+
+        """
+        filepath = os.path.join(self.cachedir, datatype, key + '.xml')
+        if not os.path.exists(filepath):
+            return False
+
+        last_modification = datetime.datetime.fromtimestamp(
+            os.path.getmtime(filepath))
+        now = datetime.datetime.now()
+
+        if (now - last_modification) > self.max_age:
+            return False
+        else:
+            return True
+
+    def _load(self, datatype, key):
+        """Read a cached version from disk.
+
+        datatype : str
+            Datatype of the DOV object.
+        key : str
+            Unique and permanent object key of the DOV object.
+
+        Returns
+        -------
+        str (xml)
+            XML string of the DOV object, loaded from the cache.
+
+        """
+        filepath = os.path.join(self.cachedir, datatype, key + '.xml')
+        with open(filepath, 'r', encoding='utf-8') as f:
+            return f.read()
+
+    def _get_remote(self, url):
+        """Get the XML data by requesting it from the given URL.
+
+        Parameters
+        ----------
+        url : str
+            Permanent URL to a DOV object.
+
+        Returns
+        -------
+        xml : bytes
+            The raw XML data of this DOV object as bytes.
+
+        """
+        return openURL(url).read()
+
+    def get(self, url):
+        """Get the XML data for the DOV object referenced by the given URL.
+
+        If a valid version exists in the cache, it will be loaded and
+        returned. If no valid version exists, the XML will be downloaded
+        from the DOV webservice, saved in the cache and returned.
+
+        Parameters
+        ----------
+        url : str
+            Permanent URL to a DOV object.
+
+        Returns
+        -------
+        xml : bytes
+            The raw XML data of this DOV object as bytes.
+
+        """
+        datatype, key = self._get_type_key(url)
+
+        if self._valid(datatype, key):
+            try:
+                return self._load(datatype, key).encode('utf-8')
+            except Exception:
+                pass
+
+        data = self._get_remote(url)
+        try:
+            self._save(datatype, key, data)
+        except Exception:
+            pass
+
+        return data
+
+    def clean(self):
+        """Clean the cache by removing old records from the cache.
+
+        Since during normal use the cache only grows by adding new objects and
+        overwriting existing ones with a new version, you can use this
+        function to clean the cache. It will remove all records older than
+        the maximum age from the cache.
+
+        Note that this method is currently not called anywhere in the code,
+        but it is provided as reference.
+
+        """
+        if os.path.exists(self.cachedir):
+            for type in os.listdir(self.cachedir):
+                for object in os.listdir(os.path.join(self.cachedir, type)):
+                    if not self._valid(type, object.rstrip('.xml')):
+                        os.remove(os.path.join(self.cachedir, type, object))
+
+    def remove(self):
+        """Remove the entire cache directory.
+
+        Note that the default directory to save the cache is a temporary
+        location provided by the operating system, and as a subsequence the
+        OS will normally take care of its removal.
+
+        Note that this method is currently not called anywhere in the code,
+        but it is provided as reference.
+
+        """
+        if os.path.exists(self.cachedir):
+            shutil.rmtree(self.cachedir)


Test patch:
diff --git a/tests/test_encoding.py b/tests/test_encoding.py
index ea8469a..111f620 100644
--- a/tests/test_encoding.py
+++ b/tests/test_encoding.py
@@ -1,19 +1,29 @@
 # -*- encoding: utf-8 -*-
+import os
+import time
+from io import open
 
 import pytest
 
+import pydov
 from owslib.fes import PropertyIsEqualTo
 from pydov.search.boring import BoringSearch
+
 from tests.abstract import (
     service_ok,
 )
 
+from tests.test_util_caching import (
+    cache,
+    nocache,
+)
 
 class TestEncoding(object):
     """Class grouping tests related to encoding issues."""
 
     @pytest.mark.online
     @pytest.mark.skipif(not service_ok(), reason="DOV service is unreachable")
+    @nocache
     def test_search(self):
         """Test the search method with strange character in the output.
 
@@ -29,3 +39,137 @@ class TestEncoding(object):
                                  return_fields=('pkey_boring', 'uitvoerder'))
 
         assert df.uitvoerder[0] == u'Societé Belge des Bétons'
+
+    @pytest.mark.online
+    @pytest.mark.skipif(not service_ok(), reason="DOV service is unreachable")
+    def test_search_cache(self, cache):
+        """Test the search method with strange character in the output.
+
+        Test whether the output has the correct encoding, both with and
+        without using the cache.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+
+        """
+        orig_cache = pydov.cache
+        pydov.cache = cache
+
+        boringsearch = BoringSearch()
+        query = PropertyIsEqualTo(
+            propertyname='pkey_boring',
+            literal='https://www.dov.vlaanderen.be/data/boring/1928-031159')
+
+        df = boringsearch.search(query=query,
+                                 return_fields=('pkey_boring', 'uitvoerder',
+                                                'mv_mtaw'))
+
+        assert df.uitvoerder[0] == u'Societé Belge des Bétons'
+
+        assert os.path.exists(os.path.join(
+            cache.cachedir, 'boring', '1928-031159.xml'))
+
+        df = boringsearch.search(query=query,
+                                 return_fields=('pkey_boring', 'uitvoerder',
+                                                'mv_mtaw'))
+
+        assert df.uitvoerder[0] == u'Societé Belge des Bétons'
+
+        pydov.cache = orig_cache
+
+    @pytest.mark.online
+    @pytest.mark.skipif(not service_ok(), reason="DOV service is unreachable")
+    def test_caching(self, cache):
+        """Test the caching of an XML containing strange characters.
+
+        Test whether the data is saved in the cache.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '1995-056089.xml')
+
+        cache.clean()
+        assert not os.path.exists(cached_file)
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/1995-056089.xml')
+        assert os.path.exists(cached_file)
+
+        with open(cached_file, 'r', encoding='utf-8') as cf:
+            cached_data = cf.read()
+            assert cached_data != ""
+
+        first_download_time = os.path.getmtime(cached_file)
+
+        time.sleep(0.5)
+        cache.get('https://www.dov.vlaanderen.be/data/boring/1995-056089.xml')
+        # assure we didn't redownload the file:
+        assert os.path.getmtime(cached_file) == first_download_time
+
+    @pytest.mark.online
+    @pytest.mark.skipif(not service_ok(), reason="DOV service is unreachable")
+    def test_save_content(self, cache):
+        """Test the caching of an XML containing strange characters.
+
+        Test if the contents of the saved document are the same as the
+        original data.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '1995-056089.xml')
+
+        cache.remove()
+        assert not os.path.exists(cached_file)
+
+        ref_data = cache.get(
+            'https://www.dov.vlaanderen.be/data/boring/1995-056089.xml')
+        assert os.path.exists(cached_file)
+
+        with open(cached_file, 'r', encoding='utf-8') as cached:
+            cached_data = cached.read().encode('utf-8')
+
+        assert cached_data == ref_data
+
+    @pytest.mark.online
+    @pytest.mark.skipif(not service_ok(), reason="DOV service is unreachable")
+    def test_reuse_content(self, cache):
+        """Test the caching of an XML containing strange characters.
+
+        Test if the contents returned by the cache are the same as the
+        original data.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '1995-056089.xml')
+
+        cache.remove()
+        assert not os.path.exists(cached_file)
+
+        ref_data = cache.get(
+            'https://www.dov.vlaanderen.be/data/boring/1995-056089.xml')
+        assert os.path.exists(cached_file)
+
+        cached_data = cache.get(
+            'https://www.dov.vlaanderen.be/data/boring/1995-056089.xml')
+
+        assert cached_data == ref_data
diff --git a/tests/test_util_caching.py b/tests/test_util_caching.py
new file mode 100644
index 0000000..66b9481
--- /dev/null
+++ b/tests/test_util_caching.py
@@ -0,0 +1,310 @@
+"""Module grouping tests for the pydov.util.caching module."""
+import datetime
+import os
+import tempfile
+from io import open
+
+import time
+
+import pytest
+
+import pydov
+from pydov.util.caching import TransparentCache
+
+
+@pytest.fixture
+def mp_remote_xml(monkeypatch):
+    """Monkeypatch the call to get the remote Boring XML data.
+
+    Parameters
+    ----------
+    monkeypatch : pytest.fixture
+        PyTest monkeypatch fixture.
+
+    """
+
+    def _get_remote_data(*args, **kwargs):
+        with open('tests/data/types/boring/boring.xml', 'r') as f:
+            data = f.read()
+            if type(data) is not bytes:
+                data = data.encode('utf-8')
+        return data
+
+    monkeypatch.setattr(pydov.util.caching.TransparentCache,
+                        '_get_remote', _get_remote_data)
+
+
+@pytest.fixture
+def cache():
+    transparent_cache = TransparentCache(
+        cachedir=os.path.join(tempfile.gettempdir(), 'pydov_tests'),
+        max_age=datetime.timedelta(seconds=1))
+    yield transparent_cache
+
+    transparent_cache.remove()
+
+
+def nocache(func):
+    """Decorator to temporarily disable caching.
+
+    Parameters
+    ----------
+    func : function
+        Function to decorate.
+
+    """
+    def wrapper(*args, **kwargs):
+        orig_cache = pydov.cache
+        pydov.cache = None
+        func(*args, **kwargs)
+        pydov.cache = orig_cache
+    return wrapper
+
+
+class TestTransparentCache(object):
+    """Class grouping tests for the pydov.util.caching.TransparentCache
+    class."""
+
+    def test_clean(self, cache, mp_remote_xml):
+        """Test the clean method.
+
+        Test whether the cached file and the cache directory are nonexistent
+        after the clean method has been called.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert os.path.exists(cached_file)
+
+        cache.clean()
+        assert os.path.exists(cached_file)
+        assert os.path.exists(cache.cachedir)
+
+        time.sleep(1.5)
+        cache.clean()
+        assert not os.path.exists(cached_file)
+        assert os.path.exists(cache.cachedir)
+
+    def test_remove(self, cache, mp_remote_xml):
+        """Test the remove method.
+
+        Test whether the cache directory is nonexistent after the remove
+        method has been called.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert os.path.exists(cached_file)
+
+        cache.remove()
+        assert not os.path.exists(cached_file)
+        assert not os.path.exists(cache.cachedir)
+
+    def test_get_save(self, cache, mp_remote_xml):
+        """Test the get method.
+
+        Test whether the document is saved in the cache.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.clean()
+        assert not os.path.exists(cached_file)
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert os.path.exists(cached_file)
+
+    def test_get_reuse(self, cache, mp_remote_xml):
+        """Test the get method.
+
+        Test whether the document is saved in the cache and reused in a
+        second function call.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.clean()
+        assert not os.path.exists(cached_file)
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert os.path.exists(cached_file)
+
+        first_download_time = os.path.getmtime(cached_file)
+
+        time.sleep(0.5)
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        # assure we didn't redownload the file:
+        assert os.path.getmtime(cached_file) == first_download_time
+
+    def test_get_invalid(self, cache, mp_remote_xml):
+        """Test the get method.
+
+        Test whether the document is saved in the cache not reused if the
+        second function call is after the maximum age of the cached file.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.clean()
+        assert not os.path.exists(cached_file)
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert os.path.exists(cached_file)
+
+        first_download_time = os.path.getmtime(cached_file)
+
+        time.sleep(1.5)
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        # assure we did redownload the file, since original is invalid now:
+        assert os.path.getmtime(cached_file) > first_download_time
+
+    def test_save_content(self, cache, mp_remote_xml):
+        """Test whether the data is saved in the cache.
+
+        Test if the contents of the saved document are the same as the
+        original data.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.clean()
+        assert not os.path.exists(cached_file)
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert os.path.exists(cached_file)
+
+        with open('tests/data/types/boring/boring.xml', 'r',
+                  encoding='utf-8') as ref:
+            ref_data = ref.read()
+
+        with open(cached_file, 'r', encoding='utf-8') as cached:
+            cached_data = cached.read()
+
+        assert cached_data == ref_data
+
+    def test_reuse_content(self, cache, mp_remote_xml):
+        """Test whether the saved data is reused.
+
+        Test if the contents returned by the cache are the same as the
+        original data.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.clean()
+        assert not os.path.exists(cached_file)
+
+        cache.get('https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert os.path.exists(cached_file)
+
+        with open('tests/data/types/boring/boring.xml', 'r') as ref:
+            ref_data = ref.read().encode('utf-8')
+
+        cached_data = cache.get(
+            'https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+
+        assert cached_data == ref_data
+
+    def test_return_type(self, cache, mp_remote_xml):
+        """The the return type of the get method.
+
+        Test wether the get method returns the data in the same datatype (
+        i.e. bytes) regardless of the data was cached or not.
+
+        Parameters
+        ----------
+        cache : pytest.fixture providing  pydov.util.caching.TransparentCache
+            TransparentCache using a temporary directory and a maximum age
+            of 1 second.
+        mp_remote_xml : pytest.fixture
+            Monkeypatch the call to the remote DOV service returning an XML
+            document.
+
+        """
+        cached_file = os.path.join(
+            cache.cachedir, 'boring', '2004-103984.xml')
+
+        cache.clean()
+        assert not os.path.exists(cached_file)
+
+        ref_data = cache.get(
+            'https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert type(ref_data) is bytes
+
+        assert os.path.exists(cached_file)
+
+        cached_data = cache.get(
+            'https://www.dov.vlaanderen.be/data/boring/2004-103984.xml')
+        assert type(cached_data) is bytes