Repository: ESMValGroup/ESMValCore

Issue:
Interpolation in `resample_hours`
**Is your feature request related to a problem? Please describe.**
Our current [`resample_hours`](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/recipe/preprocessor.html#resample-hours) is only able to subset existing data. For example, it is able to create 3-hr data from 1-hr input data by only selecting every third time point.

It would be great to extend this with interpolation capabilities, so also time points that are not in the input data can be selected.

**Would you be able to help out?**
Yes!


Discussion:

Patch:
diff --git a/conda-linux-64.lock b/conda-linux-64.lock
index 08917d47c..3888d61b9 100644
--- a/conda-linux-64.lock
+++ b/conda-linux-64.lock
@@ -1,6 +1,6 @@
 # Generated by conda-lock.
 # platform: linux-64
-# input_hash: fa2d3995f132377c678545539752f20f28ca6b8ae5fc481da604e892e2cade9f
+# input_hash: 69fbe724ec4bb47187d3f262df186f510282059d1357920abda019f36fa711f5
 @EXPLICIT
 https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81
 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda#2f4327a1cbe7f022401b236e915a5fef
@@ -8,19 +8,19 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab
 https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6
 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb
 https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda#cbbe59391138ea5ad3658c76912e147f
-https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda#10569984e7db886e4f1abc2b47ad79a1
+https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-hf3520f5_1.conda#33b7851c39c25da14f6a233a8ccbeeca
 https://conda.anaconda.org/conda-forge/linux-64/libboost-headers-1.85.0-ha770c72_1.conda#012455a6eddcbf487ef0ddd1715f0b80
 https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-hc0a3c3a_7.conda#53ebd4c833fa01cb2c6353e99f905406
 https://conda.anaconda.org/conda-forge/linux-64/pandoc-3.2-ha770c72_0.conda#8c924f0b7f3e064b1c954a08e7c32fba
 https://conda.anaconda.org/conda-forge/noarch/poppler-data-0.4.12-hd8ed1ab_0.conda#d8d7293c5b37f39b2ac32940621c6592
-https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda#d786502c97404c94d7d58d258a445a65
+https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-4_cp312.conda#dccc2d142812964fcc6abdc97b672dff
 https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8
 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29
 https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h77fa898_7.conda#abf3fec87c2563697defa759dec3d639
 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d
 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab
 https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h77fa898_7.conda#72ec1b1b04c4d15d4204ece1ecea5978
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.17-h4ab18f5_0.conda#97d60c6b52391872febd35fab0a30159
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.19-h4ab18f5_0.conda#c6dedd5eab2236f4abb59ade9fb7fd44
 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda#69b8b6202a07720f448be700e300ccf4
 https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.28.1-hd590300_0.conda#dcde58ff9a1f30b0037a2315d1846d1f
 https://conda.anaconda.org/conda-forge/linux-64/fmt-10.2.1-h00ab1b0_0.conda#35ef8bc24bd34074ebae3c943d551728
@@ -50,12 +50,12 @@ https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.8.0-h166bdaf_0.tar
 https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda#40b61aab5c7ba9ff276c41cfffe6b80b
 https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda#b26e8aa824079e1be0294e7152ca4559
 https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc
-https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda#f36c115f1ee199da648e0597ec2047ad
+https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-h4ab18f5_6.conda#27329162c0dc732bcf67a4e0cd488125
 https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318b08df404f9c9be5712aaa5a6f0bb0
 https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-hd590300_1001.conda#ec7398d21e2651e0dcb0044d03b9a339
 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda#fcea371545eda051b6deafb24889fc69
 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1
-https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-h4ab18f5_1.conda#ba01a2fef8a67913acc4ab8da101dd27
+https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.0-h4ab18f5_3.conda#12ea6d0d4ed54530eaed18e4835c1f7c
 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda#71004cbf7924e19c02746ccde9fd7123
 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036
 https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.0-hdb0a2a9_1.conda#843bbb8ace1d64ac50d64639ff38b014
@@ -71,10 +71,10 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007
 https://conda.anaconda.org/conda-forge/linux-64/xxhash-0.8.2-hd590300_0.conda#f08fb5c89edfc4aadee1c81d4cfb1fa1
 https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161070d867d1b1204ea749c8eec4ef0
 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.6.12-h2ba76a8_0.conda#da9257187c044a2a8f52507fea68a4c3
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-h36a0aea_4.conda#ce9d15eeabc21f9936410382e20c2908
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.16-h36a0aea_0.conda#2555c5ffa3a60fde5a940c5c9f4327cc
-https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h36a0aea_4.conda#bd99b76853edcc6fae6a901900bba995
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.6.14-h88a6e22_1.conda#7ed63b0e816dd1635903506ef5d2c079
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-h83b837d_6.conda#3e572eacd0ce99a59e1bb9c260ad5b20
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.16-h83b837d_2.conda#f40c698b4ea90f7fedd187c6639c818b
+https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h83b837d_6.conda#7995cb937bdac5913c8904fed6b3729d
 https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda#53fb86322bdb89496d7579fe3f02fd61
 https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.0-hed5481d_0.conda#a9ea19c48e11754899299f8123070f4e
 https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda#bd77f8da987968ec3927990495dc22e4
@@ -97,18 +97,18 @@ https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-hc051c1a_0.conda#
 https://conda.anaconda.org/conda-forge/linux-64/libzip-1.10.1-h2629f0a_3.conda#ac79812548e7e8cf61f7b0abdef01d3b
 https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda#8292dea9e022d9610a11fce5e0896ed8
 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4
-https://conda.anaconda.org/conda-forge/linux-64/s2n-1.4.13-he19d79f_0.conda#51db7e9c0cd527aea7691e7405df33bf
+https://conda.anaconda.org/conda-forge/linux-64/s2n-1.4.15-he19d79f_0.conda#4c7cc3fa1d2c5a63f9e2b1e2980a1672
 https://conda.anaconda.org/conda-forge/linux-64/spdlog-1.13.0-hd2e6256_0.conda#18f9348f064632785d54dbd1db9344bb
 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda#d453b98d9c83e71da0741bb0ff4d76bc
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6
-https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda#68c34ec6149623be41a1933ab996a209
+https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-h4ab18f5_6.conda#559d338a4234c2ad6e676f460a093e67
 https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda#4d056880988120e29d75bfff282e0f45
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.8-h96d4d28_0.conda#417d99cf69a0e6f40251815ca7622273
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.8-h21d4f22_5.conda#f9dd6e8a46f55f49eae5380d3b922b71
 https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.5-hc2324a3_1.conda#11d76bee958b1989bd1ac6ee7372ea6d
 https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.conda#39f910d205726805a958da408ca194ba
 https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb
 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844
-https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.7.2-h2aa1ff5_1.conda#3bf887827d1968275978361a6e405e4f
+https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.7.4-hfca40fe_0.conda#32ddb97f897740641d8d46a829ce1704
 https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.2-hf974151_0.conda#72724f6a78ecb15559396966226d5838
 https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_h413a1c8_0.conda#a356024784da6dfd4683dc5ecf45b155
 https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.19.0-hb90f79a_1.conda#8cdb7d41faa0260875ba92414c487e2d
@@ -116,36 +116,35 @@ https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda#6
 https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda#e71f31f8cfb0a91439f2086fc8aa0461
 https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.5-h0ab5242_0.conda#557396140c71eba588e96d597e0c61aa
 https://conda.anaconda.org/conda-forge/linux-64/nss-3.100-hca3bf56_0.conda#949c4a82290ee58b3c970cef4bcfd4ad
-https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.0-h17fec99_1.conda#d2e0ffa6c3452f0a723a0ef1b96fd1cb
-https://conda.anaconda.org/conda-forge/linux-64/python-3.11.9-hb806964_0_cpython.conda#ac68acfa8b558ed406c75e98d3428d7b
+https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.1-h17fec99_1.conda#3bf65f0d8e7322a1cfe8b670fa35ec81
+https://conda.anaconda.org/conda-forge/linux-64/python-3.12.3-hab00c5b_0_cpython.conda#2540b74d304f71d3e89c81209db4db84
 https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h7f4b329_2.conda#8f70e36268dea8eb666ef14c29bd3cda
 https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.45.3-h2c6b66d_0.conda#be7d70f2db41b674733667bdd69bd000
 https://conda.anaconda.org/conda-forge/linux-64/udunits2-2.2.28-h40f5838_3.conda#6bb8deb138f87c9d48320ac21b87e7a1
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-h8ee46fc_0.conda#077b6e8ad6a3ddb741fce2496dd01bec
 https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.16-pyhd8ed1ab_0.conda#def531a3ac77b7fb8c21d17bb5d0badb
 https://conda.anaconda.org/conda-forge/noarch/antlr-python-runtime-4.11.1-pyhd8ed1ab_0.tar.bz2#15109c4977d39ad7aa3423f57243e286
+https://conda.anaconda.org/conda-forge/linux-64/astroid-3.2.2-py312h7900ff3_0.conda#2acaebd0ea2aeaf38e996bda79545c0b
 https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda#f730d54ba9cd543666d7220c9f7ed563
 https://conda.anaconda.org/conda-forge/noarch/attrs-23.2.0-pyh71513ae_0.conda#5e4c0743c70186509d1412e03c2d8dfa
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h161de36_10.conda#a7a334cb2d24e31a9bf0e7e3d01b14cb
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.1-h63f54a0_13.conda#dd5266145d7b778c9e9a0508a503e564
-https://conda.anaconda.org/conda-forge/linux-64/backports.zoneinfo-0.2.1-py311h38be061_8.conda#5384590f14dfe6ccd02811236afc9f8e
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-ha47c788_12.conda#8420d8e495a1468f593128e5fbf6748a
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.1-h29d6fba_17.conda#c20a29ff47043ba1ec24f45dc68930bf
 https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hd590300_1.conda#f27a24d46e3ea7b70a1f98e50c62508f
-https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda#cce9e7c3f1c307f2a5fb08a2922d6164
+https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h30efb56_1.conda#45801a89533d3336a365284d93298e36
 https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333
 https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2#ebb5f5f7dc4f1a3780ef7ea7738db08c
 https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda#7f4a9e3fcff3f6356ae99244a014da6a
 https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda#f3ad426304898027fc619827ff428eca
 https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.0.0-pyhd8ed1ab_0.conda#753d29fe41bb881e4b9c004f0abf973f
-https://conda.anaconda.org/conda-forge/noarch/codespell-2.2.6-pyhd8ed1ab_0.conda#a206349b7bb7475ae580f987cb425bdd
+https://conda.anaconda.org/conda-forge/noarch/codespell-2.3.0-pyhd8ed1ab_0.conda#6e67fa19bedafa7eb7d6ea91de53e03d
 https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99
 https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441
-https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py311hb755f60_0.conda#f3a8a500a2e743ff92f418f0eaf9bf71
+https://conda.anaconda.org/conda-forge/linux-64/cython-3.0.10-py312h30efb56_0.conda#b119273bff37284cbcb9281c1e85e67d
 https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364
 https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2
 https://conda.anaconda.org/conda-forge/noarch/dill-0.3.8-pyhd8ed1ab_0.conda#78745f157d56877a2c6e7b386f66f3e2
 https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda#db16c66b759a64dc5183d69cc3745a52
 https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_0.conda#e8cd5d629f65bdf0f3bb312cde14659e
-https://conda.anaconda.org/conda-forge/noarch/dodgy-0.2.1-py_0.tar.bz2#62a69d073f7446c90f417b0787122f5b
 https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2#3cf04868fee0a029769bd41f4b2fbf2d
 https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa
 https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda#15dda3cdbf330abfe9f555d22f66db46
@@ -162,21 +161,20 @@ https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda#c0cc14
 https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2#7de5386c8fea29e76b303f37dde4c352
 https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda#f800d2da156d08e289b14e87e43c1ae5
 https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_0.conda#ff7ca04134ee8dde1d7cf491a78ef7c7
-https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py311h9547e67_1.conda#2c65bdf442b0d37aad080c8a4e0d452f
-https://conda.anaconda.org/conda-forge/linux-64/lazy-object-proxy-1.10.0-py311h459d7ec_0.conda#d39020c78fd00ed774ff9c876e8aba07
+https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py312h8572e83_1.conda#c1e71f2bc05d8e8e033aefac2c490d05
 https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5
 https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-22_linux64_openblas.conda#1a2a0cd3153464fee6646f3dd6dad9b8
-https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.7.1-hca28451_0.conda#755c7f876815003337d2c61ff5d047e5
+https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.8.0-hca28451_0.conda#f21c27f076a07907e70c49bb57bd0f20
 https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.62.2-h15f2491_0.conda#8dabe607748cb3d7002ad73cd06f1325
 https://conda.anaconda.org/conda-forge/linux-64/libpq-16.3-ha72fbe1_0.conda#bac737ae28b79cfbafd515258d97d29e
 https://conda.anaconda.org/conda-forge/linux-64/libwebp-1.4.0-h2c329e2_0.conda#80030debaa84cfc31755d53742df3ca6
 https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2#91e27ef3d05cc772ce627e51cff111c4
-https://conda.anaconda.org/conda-forge/linux-64/lxml-5.2.2-py311hc0a218f_0.conda#5a9c71f5cbdf3c5b1ad2504e13792629
-https://conda.anaconda.org/conda-forge/linux-64/lz4-4.3.3-py311h38e4bf4_0.conda#3910c815fc788621f88b2bdc0fa9f0a6
-https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py311h459d7ec_0.conda#a322b4185121935c871d201ae00ac143
+https://conda.anaconda.org/conda-forge/linux-64/lxml-5.2.2-py312hb90d8a5_0.conda#da3e0a20f8eb75072ad036198c37be61
+https://conda.anaconda.org/conda-forge/linux-64/lz4-4.3.3-py312h03f37cb_0.conda#8465027beab0db69006f744be7fb3fb6
+https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py312h98912ed_0.conda#6ff0b9582da2d4a74a1f9ae1f9ce2af6
 https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_0.tar.bz2#34fc335fc50eef0b5ea708f2b5f54e0c
 https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda#5cbee699846772cc939bef23a0d524ed
-https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.0.8-py311h52f7536_0.conda#f33f59b8130753174992f409a41e112e
+https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.0.8-py312h2492b07_0.conda#0df463266eaaa1b8a35f8fd26368c1a1
 https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19
 https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda#4eccaeba205f0aed9ac3a9ea58568ca3
 https://conda.anaconda.org/conda-forge/noarch/networkx-3.3-pyhd8ed1ab_1.conda#d335fd5704b46f4efb89a6774e81aef0
@@ -189,49 +187,44 @@ https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2#
 https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_1.conda#405678b942f2481cecdb3e010f4925d9
 https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.2-pyhd8ed1ab_0.conda#6f6cf28bf8e021933869bae3f84b8fc9
 https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda#d3483c8fc2dc2cc3f5cf43e26d60cabf
-https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py311h459d7ec_0.conda#9bc62d25dcf64eec484974a3123c9d57
+https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py312h98912ed_0.conda#3facaca6cc0f7988df3250efccd32da3
 https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2#359eeb6536da0e687af562ed265ec263
 https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.2-pyhd8ed1ab_0.tar.bz2#6784285c7e55cb7212efabc79e4c2883
-https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.9.1-pyhd8ed1ab_0.tar.bz2#0191dd7efe1a94262812770183b68892
+https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.11.1-pyhd8ed1ab_0.conda#29ff12b36df16bb66fdccd4206aaebfb
 https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda#844d9eb3b43095b031874477f7d70088
-https://conda.anaconda.org/conda-forge/noarch/pyflakes-2.5.0-pyhd8ed1ab_0.tar.bz2#1b3bef4313288ae8d35b1dfba4cd84a3
+https://conda.anaconda.org/conda-forge/noarch/pyflakes-3.2.0-pyhd8ed1ab_0.conda#0cf7fef6aa123df28adb21a590065e3d
 https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda#b7f5c092b8f9800150d998a71b76d5a1
 https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.2-pyhd8ed1ab_0.conda#b9a4dacf97241704529131a0dfc0494f
 https://conda.anaconda.org/conda-forge/noarch/pyshp-2.3.1-pyhd8ed1ab_0.tar.bz2#92a889dc236a5197612bc85bee6d7174
 https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025
 https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.19.1-pyhd8ed1ab_0.conda#4d3ceee3af4b0f9a1f48f57176bf8625
 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d
-https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.4.1-py311h459d7ec_0.conda#60b5332b3989fda37884b92c7afd6a91
+https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.4.1-py312h98912ed_0.conda#a8f9739e0ada2320148c92ddd608864f
 https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad
-https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda#52719a74ad130de8fb5d047dc91f247a
-https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.18.1-py311h5ecf98a_0.conda#9ce82e95681cb5c5e4bd872ed0a7aceb
-https://conda.anaconda.org/conda-forge/noarch/semver-3.0.2-pyhd8ed1ab_0.conda#5efb3fccda53974aed800b6d575f72ed
-https://conda.anaconda.org/conda-forge/noarch/setoptconf-tmp-0.3.1-pyhd8ed1ab_0.tar.bz2#af3e36d4effb85b9b9f93cd1db0963df
-https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda#7462280d81f639363e6e63c81276bd9e
+https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py312h98912ed_1.conda#e3fd78d8d490af1d84763b9fe3f2e552
+https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.18.1-py312h4413252_0.conda#73da42918aaeb87d5618f82e2ac18d1f
+https://conda.anaconda.org/conda-forge/noarch/setuptools-70.0.0-pyhd8ed1ab_0.conda#c8ddb4f34a208df4dd42509a0f6a1c89
 https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2
-https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.0-pyhd8ed1ab_0.tar.bz2#62f26a3d1387acee31322208f0cfa3e0
 https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e
 https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_0.tar.bz2#6d6552722448103793743dabfbda532d
 https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda#3f144b2c34f8cb5a9abd9ed23a39c561
 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_0.conda#da1d979339e2714c30a8e806a33ec087
-https://conda.anaconda.org/conda-forge/noarch/sqlparse-0.5.0-pyhd8ed1ab_0.conda#4dd428bd295ba44babd13050f2bcc622
 https://conda.anaconda.org/conda-forge/noarch/tblib-3.0.0-pyhd8ed1ab_0.conda#04eedddeb68ad39871c8127dd1c21f4f
 https://conda.anaconda.org/conda-forge/noarch/termcolor-2.4.0-pyhd8ed1ab_0.conda#a5033708ad9283907c3b1bc1f90d0d0d
 https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095
 https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96
 https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.5-pyha770c72_0.conda#e5dde5caf905e9d95895e05f94967e14
 https://conda.anaconda.org/conda-forge/noarch/toolz-0.12.1-pyhd8ed1ab_0.conda#2fcb582444635e2c402e8569bb94e039
-https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4-py311h459d7ec_0.conda#cc7727006191b8f3630936b339a76cd0
+https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4-py312h98912ed_0.conda#e8332e534dca8c5c12c8352e0a23501c
 https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_0.conda#3df84416a021220d8b5700c613af2dc5
 https://conda.anaconda.org/conda-forge/noarch/types-pyyaml-6.0.12.20240311-pyhd8ed1ab_0.conda#df5d4b66033ecb54c7a4040627215529
 https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda#6ef2fc37559256cf682d8b3375e89b80
-https://conda.anaconda.org/conda-forge/linux-64/ujson-5.10.0-py311h4332511_0.conda#442a260df22ffad7f666c7e3f119b5ab
+https://conda.anaconda.org/conda-forge/linux-64/ujson-5.10.0-py312h7070661_0.conda#dd19f5820a3fd57aea70aaf88e6dd191
 https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-py_0.tar.bz2#1447ead40f2a01733a9c8dfc32988375
 https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda#68f0738df502a14213624b288c60c9ad
 https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda#daf5160ff9cde3a468556965329085b9
 https://conda.anaconda.org/conda-forge/noarch/webob-1.8.7-pyhd8ed1ab_0.tar.bz2#a8192f3585f341ea66c60c189580ac67
 https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda#0b5293a157c2b5cd513dd1b03d8d3aae
-https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.16.0-py311h459d7ec_0.conda#6669b5529d206c1f880b642cdd17ae05
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda#82b6df12252e6f32402b96dacc656fec
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda#ed67c36f215b310412b2af935bf3e530
 https://conda.anaconda.org/conda-forge/noarch/xyzservices-2024.4.0-pyhd8ed1ab_0.conda#93dffc47dadbe36a1a644f3f50d4979d
@@ -239,49 +232,46 @@ https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h75354e8_4.conda#03
 https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_0.conda#cf30c2c15b82aacb07f9c09e28ff2275
 https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a
 https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.4-pyhd8ed1ab_0.conda#46a2e6e3dfa718ce3492018d5a110dd6
-https://conda.anaconda.org/conda-forge/noarch/asgiref-3.8.1-pyhd8ed1ab_0.conda#b5c2e1034ccc76fb14031637924880eb
-https://conda.anaconda.org/conda-forge/linux-64/astroid-2.15.8-py311h38be061_0.conda#46d70fcb74472aab178991f0231ee3c6
 https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda#5f25798dcefd8252ce5f9dc494d5f571
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.20-h5f1c8d9_0.conda#418775183961dc1ee1c326a473118f98
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hcc7299c_2.conda#7003778c651fa3ba815cfdf065d769af
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.22-h96bc93b_2.conda#de2b7c9aa9b279cca5542134b7a2b86a
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-h759edc4_4.conda#8ced661d9dcece8698922fd8a73b6511
 https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.11.1-h91d86a7_1.conda#2dbab1d281b7e1da05eee544cbdc8af6
 https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e
 https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda#332493000404d8411859539a5a630865
 https://conda.anaconda.org/conda-forge/noarch/bleach-6.1.0-pyhd8ed1ab_0.conda#0ed9d7c0e9afa7c025807a9a8136ea3e
 https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda#f907bb958910dc404647326ca80c263e
-https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py311hb3a22ac_0.conda#b3469563ac5e808b0cd92810d0697043
+https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py312hf06ca03_0.conda#56b0ca764ce23cc54f3f7e2a7b970f6d
 https://conda.anaconda.org/conda-forge/linux-64/cfitsio-4.4.0-hbdc6101_1.conda#0ba5a427a51923dcdfe1121115ac8293
 https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2#4fd2c6b53934bd7d96d1f3fdaf99b79f
 https://conda.anaconda.org/conda-forge/noarch/cligj-0.7.2-pyhd8ed1ab_1.tar.bz2#a29b7c141d6b2de4bb67788a5f107734
-https://conda.anaconda.org/conda-forge/linux-64/coverage-7.5.1-py311h331c9d8_0.conda#9f35e13e3b9e05e153b78f42662061f6
-https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py311h459d7ec_0.conda#13d385f635d7fbe9acc93600f67a6cb4
+https://conda.anaconda.org/conda-forge/linux-64/coverage-7.5.3-py312h9a8786e_0.conda#f01930d0afe8ac5f8062c98e6b8d1fd0
+https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.3-py312h98912ed_0.conda#a4fbffb84a54767266c69e3699078a00
 https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.5-pyhd8ed1ab_0.conda#3a941b6083e945aa87e739a9b85c82e9
 https://conda.anaconda.org/conda-forge/noarch/fire-0.6.0-pyhd8ed1ab_0.conda#e9ed10aa8fa1dd6782940b95c942a6ae
-https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.51.0-py311h459d7ec_0.conda#17e1997cc17c571d5ad27bd0159f616c
+https://conda.anaconda.org/conda-forge/noarch/flake8-7.0.0-pyhd8ed1ab_0.conda#15bc58c860fc0a9abc26ec902df35252
+https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.53.0-py312h9a8786e_0.conda#8490346e9d5efd7a6869582aa0c95b25
 https://conda.anaconda.org/conda-forge/noarch/geopy-2.4.1-pyhd8ed1ab_1.conda#358c17429c97883b2cb9ab5f64bc161b
-https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.11-pyhd8ed1ab_0.conda#623b19f616f2ca0c261441067e18ae40
-https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.3-nompi_h4f84152_101.conda#7e98860d08eea82c8057abd78864fcb4
+https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.3-nompi_hdf9ad27_102.conda#d8cb3688b92e891e1e5f613517a50ca8
 https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.1.0-pyha770c72_0.conda#0896606848b2dc5cebdf111b6543aa04
 https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.0-pyhd8ed1ab_0.conda#c5d3907ad8bd7bf557521a1833cf7e6d
 https://conda.anaconda.org/conda-forge/noarch/isodate-0.6.1-pyhd8ed1ab_0.tar.bz2#4a62c93c1b5c0b920508ae3fd285eaf5
 https://conda.anaconda.org/conda-forge/noarch/isort-5.13.2-pyhd8ed1ab_0.conda#1d25ed2b95b92b026aaa795eabec8d91
 https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.1-pyhd8ed1ab_0.conda#81a3be0b2023e1ea8555781f0ad904a2
 https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda#7b86ecb7d3557821c649b3c31e3eb9f2
-https://conda.anaconda.org/conda-forge/linux-64/jupyter_core-5.7.2-py311h38be061_0.conda#f85e78497dfed6f6a4b865191f42de2e
+https://conda.anaconda.org/conda-forge/linux-64/jupyter_core-5.7.2-py312h7900ff3_0.conda#eee5a2e3465220ed87196bbb5665f420
 https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_1.conda#afcd1b53bcac8844540358e33f33d28f
 https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2#8d67904973263afd2985ba56aa2d6bb4
 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-22_linux64_openblas.conda#4b31699e0ec5de64d5896e580389c9a1
 https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h119a65a_9.conda#cfebc557e54905dadc355c0e9f003004
-https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.23.0-h9be4e54_1.conda#1042d8401bb268553f98e60120cdeb40
+https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.24.0-h2736e30_0.conda#34aeee3fa7fca5dc21fad3ac6f4f0ab2
 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-22_linux64_openblas.conda#b083767b6c877e24ee597d93b87ab838
-https://conda.anaconda.org/conda-forge/noarch/logilab-common-1.7.3-py_0.tar.bz2#6eafcdf39a7eb90b6d951cfff59e8d3b
 https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_0.conda#779345c95648be40d22aaa89de7d4254
-https://conda.anaconda.org/conda-forge/linux-64/mypy-1.10.0-py311h331c9d8_0.conda#fe352f306f60a49679773f07759eab09
+https://conda.anaconda.org/conda-forge/linux-64/mypy-1.10.0-py312h9a8786e_0.conda#644f1c538810d1f250ef0fc844f3822d
 https://conda.anaconda.org/conda-forge/noarch/nested-lookup-0.2.25-pyhd8ed1ab_1.tar.bz2#2f59daeb14581d41b1e2dda0895933b2
 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.8.0-pyhd8ed1ab_0.conda#2a75b296096adabbabadd5e9782e5fcc
 https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda#0badf9c54e24cecfb0ad2f99d680c163
 https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_0.conda#629f3203c99b32e0988910c93e77f3b6
-https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py311h18e6fac_0.conda#6c520a9d36c9d7270988c7a6c360d6d4
+https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py312hdcec9eb_0.conda#425bb325f970e57a047ac57c4586489d
 https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda#f586ac1e56c8638b64f9c8122a7b8a67
 https://conda.anaconda.org/conda-forge/linux-64/postgresql-16.3-h8e811e2_0.conda#e4d52462da124ed3792472f95a36fc2a
 https://conda.anaconda.org/conda-forge/linux-64/proj-9.4.0-h1d62c97_1.conda#113f894e5019db2e2705645ee3bcf91a
@@ -289,7 +279,7 @@ https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0
 https://conda.anaconda.org/conda-forge/noarch/pydocstyle-6.3.0-pyhd8ed1ab_0.conda#7e23a61a7fbaedfef6eb0e1ac775c8e5
 https://conda.anaconda.org/conda-forge/noarch/pytest-8.2.1-pyhd8ed1ab_0.conda#e4418e8bdbaa8eea28e047531e6763c8
 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda#2cf4264fffb9e6eff6031c5b6884d61c
-https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.0.3-py311h08a0b41_0.conda#8bef21c0a0160e7369fc2f494acf85d0
+https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.0.3-py312h8fd38d8_0.conda#27efa6d21e98bcab4585a6b913df7625
 https://conda.anaconda.org/conda-forge/noarch/referencing-0.35.1-pyhd8ed1ab_0.conda#0fc8b52192a8898627c3efae1003e9f6
 https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda#8662629d9a05f9cff364e31ca106c1ac
 https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.11.0-hd8ed1ab_0.conda#471e3988f8ca5e9eb3ce6be7eac3bcee
@@ -299,114 +289,107 @@ https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.26.2-pyhd8ed1ab_0.co
 https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.2.5-hac6953d_0.conda#63b80ca78d29380fe69e69412dcbe4ac
 https://conda.anaconda.org/conda-forge/noarch/yamale-5.0.0-pyhca7485f_0.conda#53c70f2dd94c690afd8606de7e952fe7
 https://conda.anaconda.org/conda-forge/noarch/yamllint-1.35.1-pyhd8ed1ab_0.conda#a1240b99a7ccd953879dc63111823986
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.5.8-h10bd90f_3.conda#f651b434355a203d2a3d0e4c4c329d9b
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.5.9-h594631b_3.conda#47490db1dcddfb1c355251fc427746a6
 https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.6.0-hf1915f5_1.conda#fd11ea65ceb397f9587b1d88a4329d73
 https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.5.0-h94269e2_4.conda#f364272cb4c2f4ce2341067107b82865
 https://conda.anaconda.org/conda-forge/noarch/cattrs-23.2.3-pyhd8ed1ab_0.conda#91fc4700dcce4a46d439900a132fe4e5
-https://conda.anaconda.org/conda-forge/linux-64/cryptography-42.0.7-py311h4a61cc7_0.conda#251f3a7b306a8b212d42d585b3d06f36
-https://conda.anaconda.org/conda-forge/noarch/django-5.0.6-pyhd8ed1ab_0.conda#8451875d7b41dbab4d282f02719f3d6d
-https://conda.anaconda.org/conda-forge/noarch/flake8-5.0.4-pyhd8ed1ab_0.tar.bz2#8079ea7dec0a917dd0cb6c257f7ea9ea
-https://conda.anaconda.org/conda-forge/linux-64/geotiff-1.7.1-h928be8b_17.conda#4b2ca09b53e91b760cebcb397e386e87
-https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.43-pyhd8ed1ab_0.conda#0b2154c1818111e17381b1df5b4b0176
+https://conda.anaconda.org/conda-forge/linux-64/cryptography-42.0.7-py312hbcc2302_0.conda#7bc0e1aae21b2e82d03959931f4294f0
+https://conda.anaconda.org/conda-forge/linux-64/geotiff-1.7.3-h928be8b_0.conda#c0f2468661b7cae54a7a1ff11926e372
 https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.5.0-hfac3d4d_0.conda#f5126317dd0ce0ba26945e411ecc6960
 https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.1.0-hd8ed1ab_0.conda#6ef2b72d291b39e479d7694efa2b2b98
 https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2023.12.1-pyhd8ed1ab_0.conda#a0e4efb5f35786a05af4809a2fb1f855
 https://conda.anaconda.org/conda-forge/linux-64/kealib-1.5.3-hee9dde6_1.conda#c5b7b29e2b66107553d0366538257a51
-https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.23.0-hc7a4891_1.conda#ee99fb9107ffb579b58ee92a5fb14b06
+https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.24.0-h3d9a0c8_0.conda#a731371833a7b1ab3a87be0fe7e6235a
 https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.2-nompi_h9612171_113.conda#b2414908e43c442ddc68e6148774a304
-https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-h6f065fc_5.conda#f2e2bdd5fd10493a525503b2f40c59eb
-https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py311h64a7726_0.conda#a502d7aad449a1206efb366d6a12c52d
+https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-h5539517_6.conda#1ee26233875c04444bdb2e5a838b5634
+https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py312heda63a1_0.conda#d8285bea2a350f63fab23bf460221f3f
 https://conda.anaconda.org/conda-forge/linux-64/poppler-24.04.0-hb6cd0d7_0.conda#d19eed746748f1d44b575662f2bcfe95
 https://conda.anaconda.org/conda-forge/noarch/pybtex-0.24.0-pyhd8ed1ab_2.tar.bz2#2099b86a7399c44c0c61cdb6de6915ba
-https://conda.anaconda.org/conda-forge/noarch/pylint-2.17.7-pyhd8ed1ab_0.conda#3cab6aee60038b3f621bce3e50f52bed
-https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.6.1-py311hb3a3e68_6.conda#bce79adf84cd7cd9b9b14e43d6d52daf
+https://conda.anaconda.org/conda-forge/noarch/pylint-3.2.2-pyhd8ed1ab_0.conda#6621f1cfd6f9669482be177467ebe919
+https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.6.1-py312hb591178_6.conda#e5a041de2ecaee7e02c40cf82afa132e
 https://conda.anaconda.org/conda-forge/noarch/pytest-cov-5.0.0-pyhd8ed1ab_0.conda#c54c0107057d67ddf077751339ec2c63
 https://conda.anaconda.org/conda-forge/noarch/pytest-env-1.1.3-pyhd8ed1ab_0.conda#1dbdf019d740419852c4a7803fff49d9
 https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_0.conda#52b91ecba854d55b28ad916a8b10da24
 https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.14.0-pyhd8ed1ab_0.conda#4b9b5e086812283c052a9105ab1e254e
+https://conda.anaconda.org/conda-forge/noarch/pytest-mypy-0.10.3-pyhd8ed1ab_0.conda#37fd5d5468805e017650a395ca779f38
 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.5.0-pyhd8ed1ab_0.conda#d5f595da2daead898ca958ac62f0307b
 https://conda.anaconda.org/conda-forge/noarch/rdflib-7.0.0-pyhd8ed1ab_0.conda#44d14ef95495b3d4438f28998e0296a9
-https://conda.anaconda.org/conda-forge/noarch/requests-2.32.1-pyhd8ed1ab_0.conda#20ce56db6c7aae9e6654b358ecbfc470
-https://conda.anaconda.org/conda-forge/noarch/requirements-detector-1.2.2-pyhd8ed1ab_0.conda#6626918380d99292df110f3c91b6e5ec
+https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda#5ede4753180c7a550a443c430dc8ab52
 https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda#e7df0fdd404616638df5ece6e69ba7af
-https://conda.anaconda.org/conda-forge/noarch/types-requests-2.31.0.20240406-pyhd8ed1ab_0.conda#b4bc9b6dbc54191100b518a18be6045e
-https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.0.1-py311h9547e67_4.conda#586da7df03b68640de14dc3e8bcbf76f
+https://conda.anaconda.org/conda-forge/noarch/types-requests-2.32.0.20240523-pyhd8ed1ab_0.conda#fbfe8eaee3a9ccbd55ac49170cf2c063
+https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.0.1-py312h8572e83_4.conda#52c9e25ee0a32485a102eeecdb7eef52
 https://conda.anaconda.org/conda-forge/noarch/yapf-0.40.1-pyhd8ed1ab_0.conda#f269942e802d5e148632143d4c37acc9
-https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.26.8-h02fd9b4_10.conda#289d899cba63e36d6768988aac83fcd4
+https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.26.9-he3a8b3b_0.conda#fbe6a256dd70a505730e7c461cd37a35
 https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.10.0-h00ab1b0_1.conda#1e63d3866554a4d2e3d1cba5f21a2841
-https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.3-py311h18e1886_1.conda#f1beb063aad4446eb146d8b88420a4ea
-https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py311h9547e67_0.conda#74ad0ae64f1ef565e27eda87fa749e84
-https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.5.1-pyhd8ed1ab_0.conda#d4f60ccc5421472d2583efd9ce39d8b1
-https://conda.anaconda.org/conda-forge/noarch/flake8-polyfill-1.0.2-py_0.tar.bz2#a53db35e3d07f0af2eccd59c2a00bffe
+https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.3-py312h085067d_1.conda#b121b9dd4935f63959eb35cc6c36973b
+https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.2.1-py312h8572e83_0.conda#12c6a831ef734f0b2dd4caff514cbb7f
+https://conda.anaconda.org/conda-forge/noarch/dask-core-2024.5.2-pyhd8ed1ab_0.conda#1a57a819915e1c169b74933720b138f2
 https://conda.anaconda.org/conda-forge/noarch/identify-2.5.36-pyhd8ed1ab_0.conda#ba68cb5105760379432cebc82b45af40
-https://conda.anaconda.org/conda-forge/noarch/ipython-8.24.0-pyh707e725_0.conda#1fb1f1fcbe053a762748dbf0ae4cfd0d
+https://conda.anaconda.org/conda-forge/noarch/ipython-8.25.0-pyh707e725_0.conda#98466a37c08f3bdbb500786271859517
 https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.22.0-pyhd8ed1ab_0.conda#b9661a4b1200d6bc7d8a4cdafdc91468
-https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.1-pyhd8ed1ab_0.conda#c03972cfce69ad913d520c652e5ed908
+https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.2-pyhd8ed1ab_0.conda#3cdbb2fa84490e5fd44c9f9806c0d292
 https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.6.1-nompi_hacb5139_103.conda#50f05f98d084805642d24dff910e11e8
-https://conda.anaconda.org/conda-forge/linux-64/pandas-2.1.4-py311h320fe9a_0.conda#e44ccb61b6621bf3f8053ae66eba7397
+https://conda.anaconda.org/conda-forge/linux-64/pandas-2.1.4-py312hfb8ada1_0.conda#d0745ae74c2b26571b692ddde112eebb
 https://conda.anaconda.org/conda-forge/linux-64/pango-1.52.2-ha41ecd1_0.conda#a658eeabf188c3040da36b0763de2bfd
 https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.1-pyhd8ed1ab_0.conda#d15917f33140f8d2ac9ca44db7ec8a25
-https://conda.anaconda.org/conda-forge/noarch/pylint-plugin-utils-0.7-pyhd8ed1ab_0.tar.bz2#1657976383aee04dbb3ae3bdf654bb58
 https://conda.anaconda.org/conda-forge/noarch/pyopenssl-24.0.0-pyhd8ed1ab_0.conda#b50aec2c744a5c493c09cce9e2e7533e
 https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_0.conda#4d2040212307d18392a2687772b3a96d
 https://conda.anaconda.org/conda-forge/noarch/requests-cache-1.2.0-pyhd8ed1ab_1.conda#f351ee57bb9c3e4b2f4952980461ba1d
-https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.0-py311h517d4fd_1.conda#a86b8bea39e292a23b2cf9a750f49ea1
-https://conda.anaconda.org/conda-forge/linux-64/shapely-2.0.4-py311h0bed3d6_1.conda#6fb2f733ef405b4bfb4a6a362703457e
-https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.267-h51dfee4_8.conda#188857656abd6d1a4dcc471c619b0de5
+https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.1-py312hc2bc53b_0.conda#864b2399a9c998e17d1a9a4e0c601285
+https://conda.anaconda.org/conda-forge/linux-64/shapely-2.0.4-py312ha5b4d35_1.conda#1248b799f811d8ea215de88f53ae7ffc
+https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.329-hba8bd5f_3.conda#720494d9f06b4aff1270cffb7acc7920
 https://conda.anaconda.org/conda-forge/noarch/bokeh-3.4.1-pyhd8ed1ab_0.conda#0f8e0831bbf38d83973438ce9af9af9a
-https://conda.anaconda.org/conda-forge/linux-64/cf-units-3.2.0-py311h18e1886_5.conda#6cd3facab7a79de14abb1a86a2d830fa
-https://conda.anaconda.org/conda-forge/noarch/distributed-2024.5.1-pyhd8ed1ab_0.conda#d6b959de6c4ed39dd0a09fbe74d37ca4
+https://conda.anaconda.org/conda-forge/linux-64/cf-units-3.2.0-py312h085067d_5.conda#b40cdf87aee69ccf162022579cb99afb
+https://conda.anaconda.org/conda-forge/noarch/distributed-2024.5.2-pyhd8ed1ab_0.conda#2fa6807bd19e5cdc77fe1b6a42c86228
 https://conda.anaconda.org/conda-forge/linux-64/esmf-8.4.2-nompi_h9e768e6_3.conda#c330e87e698bae8e7381c0315cf25dd0
 https://conda.anaconda.org/conda-forge/linux-64/gtk2-2.24.33-h280cfa0_4.conda#410f86e58e880dcc7b0e910a8e89c05c
 https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.0-hadf69e7_1.conda#0e2b5bd9533043b41f9482ae9e2c16b5
-https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py311ha4ca890_2.conda#0848e2084cbb57014f232f48568561af
+https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.8.4-py312h20ab3a6_2.conda#fbfe798f83f0d66410903ad8f40d5283
 https://conda.anaconda.org/conda-forge/noarch/myproxyclient-2.1.1-pyhd8ed1ab_0.conda#bcdbeb2b693eba886583a907840c6421
 https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_0.conda#0b57b5368ab7fc7cdc9e3511fa867214
-https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.6.5-nompi_py311h74118c1_101.conda#153b3a81c6f11e87b87461247c3cf061
-https://conda.anaconda.org/conda-forge/noarch/pep8-naming-0.10.0-pyh9f0ad1d_0.tar.bz2#b3c5536e4f9f58a4b16adb6f1e11732d
+https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.6.5-nompi_py312h39d4375_101.conda#9033de6c10fd3396990890d8d8a6ac4e
 https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.7.1-pyha770c72_0.conda#724bc4489c1174fc8e3233b0624fa51f
-https://conda.anaconda.org/conda-forge/noarch/pylint-celery-0.3-py_1.tar.bz2#e29456a611a62d3f26105a2f9c68f759
-https://conda.anaconda.org/conda-forge/noarch/pylint-django-2.5.3-pyhd8ed1ab_0.tar.bz2#00d8853fb1f87195722ea6a582cc9b56
-https://conda.anaconda.org/conda-forge/noarch/pylint-flask-0.6-py_0.tar.bz2#5a9afd3d0a61b08d59eed70fab859c1b
-https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.3.0-py311h1f0f07a_1.conda#cd36a89a048ad2bcc6d8b43f648fb1d0
+https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.3.0-py312hc7c0aa3_1.conda#81789bd582b389e77ff709f6b27300fb
 https://conda.anaconda.org/conda-forge/noarch/xarray-2024.5.0-pyhd8ed1ab_0.conda#e839fd0ae78a368c930f0b1feafa6736
-https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.23.0-py311h14de704_1.conda#27e5956e552c6e71f56cb1ec042617a8
-https://conda.anaconda.org/conda-forge/noarch/cf_xarray-0.9.0-pyhd8ed1ab_0.conda#33070a578d45591f242a254f78f86f10
+https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.23.0-py312h1d6d2e6_1.conda#6392d3ce615ab0f32bc39b07f8f4c300
+https://conda.anaconda.org/conda-forge/noarch/cf_xarray-0.9.1-pyhd8ed1ab_0.conda#9b1ddfcc35856fbd69e193b945b7d7f2
 https://conda.anaconda.org/conda-forge/noarch/dask-jobqueue-0.8.5-pyhd8ed1ab_0.conda#abfb434fb6654f83d740428863ec85a8
 https://conda.anaconda.org/conda-forge/noarch/esgf-pyclient-0.3.1-pyhca7485f_3.conda#1d43833138d38ad8324700ce45a7099a
 https://conda.anaconda.org/conda-forge/noarch/esmpy-8.4.2-pyhc1e730c_4.conda#ddcf387719b2e44df0cc4dd467643951
 https://conda.anaconda.org/conda-forge/linux-64/graphviz-11.0.0-hc68bbd7_0.conda#52a531ef95358086a56086c45d97ab75
-https://conda.anaconda.org/conda-forge/linux-64/libarrow-16.1.0-hefa796f_1_cpu.conda#9b96a5f974932e802cdeda54733b4c53
+https://conda.anaconda.org/conda-forge/linux-64/libarrow-16.1.0-hcb6531f_6_cpu.conda#0df3fc2a8d63b1cc49973c5a679ec438
 https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.0-pyhd8ed1ab_0.conda#15b51397e0fe8ea7d7da60d83eb76ebc
 https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.1-pyhd8ed1ab_0.tar.bz2#281b58948bf60a2582de9e548bcc5369
-https://conda.anaconda.org/conda-forge/noarch/prospector-1.10.3-pyhd8ed1ab_0.conda#f551d4d859a1d70c6abff8310a655481
-https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.23.0-h27f064a_0.conda#8a4a8fd1af3cfbdcb54ea7282965d91a
+https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.23.0-hfa691db_2.conda#f9cd15d6c7deeeb5b60d65fac59b18bc
 https://conda.anaconda.org/conda-forge/noarch/iris-3.9.0-pyha770c72_0.conda#efaf150eb009f04efa58f1401c767192
-https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-16.1.0-hac33072_1_cpu.conda#fcdd73184457b7055f7ca16ffc6d6711
-https://conda.anaconda.org/conda-forge/linux-64/libgdal-3.8.5-h77540a9_7.conda#5937eafdfa5713c78a21cbb33a84539e
-https://conda.anaconda.org/conda-forge/linux-64/libparquet-16.1.0-h6a7eafb_1_cpu.conda#cb6a5bc92eac2f308c8fb6a2b1ff057c
+https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-16.1.0-hac33072_6_cpu.conda#38b1161e2f8c72095f64ea35ee1294c5
+https://conda.anaconda.org/conda-forge/linux-64/libgdal-3.9.0-h77540a9_5.conda#3703d3fb83229c78852cba9c37482caa
+https://conda.anaconda.org/conda-forge/linux-64/libparquet-16.1.0-h6a7eafb_6_cpu.conda#87f676c6cb33f8e1956948ee216fa3a1
 https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.4-pyhd8ed1ab_0.conda#43d9cd74e3950ab09cbddf36f1706b9f
 https://conda.anaconda.org/conda-forge/noarch/py-cordex-0.7.0-pyhd8ed1ab_0.conda#7ad60b498674a9bff3ba8f3fb335e4f0
-https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-16.1.0-py311h8e2c35d_0_cpu.conda#c50b8f4436f4fc7b599122d14225322b
-https://conda.anaconda.org/conda-forge/linux-64/pydot-2.0.0-py311h38be061_0.conda#cdfd23a54a18f3c8d5320d7717f4ed52
-https://conda.anaconda.org/conda-forge/linux-64/gdal-3.8.5-py311hf92cf48_7.conda#99b21fe537bd3afa6fc28949cc2d9ba5
-https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-16.1.0-hac33072_1_cpu.conda#855feea87f42887b22accedd41fa0755
+https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-16.1.0-py312h5429d62_1_cpu.conda#cee0cddfaedfd3657f429318207e5816
+https://conda.anaconda.org/conda-forge/linux-64/pydot-2.0.0-py312h7900ff3_0.conda#a3628f9a0ca8573314dc6f94f40a0419
+https://conda.anaconda.org/conda-forge/linux-64/gdal-3.9.0-py312h86af8fa_5.conda#fa187303265629d7229baaa6b44d333d
+https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-16.1.0-hac33072_6_cpu.conda#2e9430df8ffd645a5bc7edffb252c3de
 https://conda.anaconda.org/conda-forge/noarch/nbconvert-pandoc-7.16.4-hd8ed1ab_0.conda#391934bd1a79990c23df1d1809ddc821
 https://conda.anaconda.org/conda-forge/noarch/prov-2.0.0-pyhd3deb0d_0.tar.bz2#aa9b3ad140f6c0668c646f32e20ccf82
-https://conda.anaconda.org/conda-forge/linux-64/fiona-1.9.6-py311h4c8953a_1.conda#896652a042ee508365292b99e6cea677
-https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-16.1.0-h7e0c224_1_cpu.conda#0f04b3b2867e724c72e20a29f2f76169
+https://conda.anaconda.org/conda-forge/linux-64/fiona-1.9.6-py312h32ad294_3.conda#6da62c5c06a6416e0130220e4f418bb0
+https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-16.1.0-h7e0c224_6_cpu.conda#81fea801c4bb126509e784cbd2ca4d17
 https://conda.anaconda.org/conda-forge/noarch/nbconvert-7.16.4-hd8ed1ab_0.conda#c9d64b8a7ee8e6bdbf0e7d8aa7f39601
-https://conda.anaconda.org/conda-forge/linux-64/pyarrow-16.1.0-py311h781c19f_0.conda#bd1348ebc8a3f45a18f5e3f01483a628
-https://conda.anaconda.org/conda-forge/noarch/dask-expr-1.1.1-pyhd8ed1ab_1.conda#00a6a9a6c58075008515a106625047cf
+https://conda.anaconda.org/conda-forge/linux-64/pyarrow-16.1.0-py312h8da182e_1.conda#2d8b51007ba9ec982067ecfc74315c3a
+https://conda.anaconda.org/conda-forge/noarch/dask-expr-1.1.2-pyhd8ed1ab_0.conda#34db694d2afc672094f1a74af51cb44e
 https://conda.anaconda.org/conda-forge/noarch/pyarrow-hotfix-0.6-pyhd8ed1ab_0.conda#ccc06e6ef2064ae129fab3286299abda
-https://conda.anaconda.org/conda-forge/noarch/dask-2024.5.1-pyhd8ed1ab_0.conda#2b7c338ca2077c3131871523fa38d4ed
-https://conda.anaconda.org/conda-forge/noarch/iris-esmf-regrid-0.9.0-pyhd8ed1ab_0.conda#570f2c6e387fd6dac5356a5152f91b3f
+https://conda.anaconda.org/conda-forge/noarch/dask-2024.5.2-pyhd8ed1ab_0.conda#6bb2c18b838161e550a30de0fdff6993
+https://conda.anaconda.org/conda-forge/noarch/iris-esmf-regrid-0.10.0-pyhd8ed1ab_0.conda#a5ccce1a87da81d6c690cd11ae0687a2
 https://conda.anaconda.org/conda-forge/noarch/autodocsumm-0.2.6-pyhd8ed1ab_0.tar.bz2#4409dd7e06a62c3b2aa9e96782c49c6d
 https://conda.anaconda.org/conda-forge/noarch/nbsphinx-0.9.4-pyhd8ed1ab_0.conda#9dc80eaeff56fb67dbf4f871b81bc13a
-https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.15.2-pyhd8ed1ab_0.conda#ce99859070b0e17ccc63234ca58f3ed8
+https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.15.3-pyhd8ed1ab_0.conda#55e445f4fcb07f2471fb0e1102d36488
 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.8-pyhd8ed1ab_0.conda#611a35a27914fac3aa37611a6fe40bb5
 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.6-pyhd8ed1ab_0.conda#d7e4954df0d3aea2eacc7835ad12671d
 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.5-pyhd8ed1ab_0.conda#7e1e7437273682ada2ed5e9e9714b140
 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-1.0.7-pyhd8ed1ab_0.conda#26acae54b06f178681bfb551760f5dd1
 https://conda.anaconda.org/conda-forge/noarch/sphinx-7.3.7-pyhd8ed1ab_0.conda#7b1465205e28d75d2c0e1a868ee00a67
 https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_0.conda#e507335cb4ca9cff4c3d0fa9cdab255e
+# pip antlr4-python3-runtime @ https://files.pythonhosted.org/packages/29/14/8ac135ec7cc9db3f768e2d032776718c6b23f74e63543f0974b4873500b2/antlr4-python3-runtime-4.7.2.tar.gz#sha256=168cdcec8fb9152e84a87ca6fd261b3d54c8f6358f42ab3b813b14a7193bb50b
+# pip scitools-iris @ https://files.pythonhosted.org/packages/2a/4f/9929f74388dbc6334c3342ff5f45a862322ad1789e32d6ab292d99d81f51/scitools_iris-3.9.0-py3-none-any.whl#sha256=1a9acfb872b50554de269d403a79e01e169c9973d4ec30a46b824c8ec710a818
+# pip esmvaltool-sample-data @ https://files.pythonhosted.org/packages/58/fa/4ecc84665e0ed04c8c4c797405c19c12900bdba6438ab2f5541bf8aa1d42/ESMValTool_sample_data-0.0.3-py3-none-any.whl#sha256=81f0f02182eacb3b639cb207abae5ac469c6dd83fb6dfe6d2430c69723d85461
diff --git a/doc/quickstart/configure.rst b/doc/quickstart/configure.rst
index 834109e2d..cb5c73340 100644
--- a/doc/quickstart/configure.rst
+++ b/doc/quickstart/configure.rst
@@ -26,7 +26,7 @@ User configuration file
 
 
 The ``config-user.yml`` configuration file contains all the global level
-information needed by ESMValTool. It can be reused as many times the user needs
+information needed by ESMValCore. It can be reused as many times the user needs
 to before changing any of the options stored in it. This file is essentially
 the gateway between the user and the machine-specific instructions to
 ``esmvaltool``. By default, esmvaltool looks for it in the home directory,
@@ -189,7 +189,7 @@ and memory usage.
 A detailed explanation of the data finding-related sections of the
 ``config-user.yml`` (``rootpath`` and ``drs``) is presented in the
 :ref:`data-retrieval` section. This section relates directly to the data
-finding capabilities  of ESMValTool and are very important to be understood by
+finding capabilities of ESMValCore and are very important to be understood by
 the user.
 
 .. note::
@@ -945,7 +945,7 @@ addition of more details per project, dataset, mip table, and variable name.
 
 More precisely, one can provide this information in an extra yaml file, named
 `{project}-something.yml`, where `{project}` corresponds to the project as used
-by ESMValTool in :ref:`Datasets` and "something" is arbitrary.
+by ESMValCore in :ref:`Datasets` and "something" is arbitrary.
 
 Format of the extra facets files
 --------------------------------
@@ -998,7 +998,7 @@ variable of any CMIP5 dataset that does not have a ``product`` key yet:
 Location of the extra facets files
 ----------------------------------
 Extra facets files can be placed in several different places. When we use them
-to support a particular use-case within the ESMValTool project, they will be
+to support a particular use-case within the ESMValCore project, they will be
 provided in the sub-folder `extra_facets` inside the package
 :mod:`esmvalcore.config`. If they are used from the user side, they can be either
 placed in `~/.esmvaltool/extra_facets` or in any other directory of the users
diff --git a/doc/quickstart/find_data.rst b/doc/quickstart/find_data.rst
index 131bf8f99..5692a9f0b 100644
--- a/doc/quickstart/find_data.rst
+++ b/doc/quickstart/find_data.rst
@@ -7,7 +7,7 @@ Input data
 Overview
 ========
 Data discovery and retrieval is the first step in any evaluation process;
-ESMValTool uses a `semi-automated` data finding mechanism with inputs from both
+ESMValCore uses a `semi-automated` data finding mechanism with inputs from both
 the user configuration file and the recipe file: this means that the user will
 have to provide the tool with a set of parameters related to the data needed
 and once these parameters have been provided, the tool will automatically find
@@ -31,7 +31,7 @@ standard for naming files and structured paths; the `DRS
 <https://www.ecmwf.int/sites/default/files/elibrary/2014/13713-data-reference-syntax-governing-standards-within-climate-research-data-archived-esgf.pdf>`_
 ensures that files and paths to them are named according to a
 standardized convention. Examples of this convention, also used by
-ESMValTool for file discovery and data retrieval, include:
+ESMValCore for file discovery and data retrieval, include:
 
 * CMIP6 file: ``{variable_short_name}_{mip}_{dataset_name}_{experiment}_{ensemble}_{grid}_{start-date}-{end-date}.nc``
 * CMIP5 file: ``{variable_short_name}_{mip}_{dataset_name}_{experiment}_{ensemble}_{start-date}-{end-date}.nc``
@@ -44,7 +44,7 @@ ESGF data nodes, these paths differ slightly, for example:
   {variable_short_name}/{grid}``;
 * CMIP6 path for ETHZ: ``ROOT-ETHZ/{experiment}/{mip}/{variable_short_name}/{dataset_name}/{ensemble}/{grid}``
 
-From the ESMValTool user perspective the number of data input parameters is
+From the ESMValCore user perspective the number of data input parameters is
 optimized to allow for ease of use. We detail this procedure in the next
 section.
 
@@ -163,7 +163,7 @@ dedicated projects instead of the project ``native6``.
 CESM
 ^^^^
 
-ESMValTool is able to read native `CESM <https://www.cesm.ucar.edu/>`__ model
+ESMValCore is able to read native `CESM <https://www.cesm.ucar.edu/>`__ model
 output.
 
 .. warning::
@@ -252,7 +252,7 @@ Key                  Description                            Default value if not
 EMAC
 ^^^^
 
-ESMValTool is able to read native `EMAC
+ESMValCore is able to read native `EMAC
 <https://www.dlr.de/pa/en/desktopdefault.aspx/tabid-8859/15306_read-37415/>`_
 model output.
 
@@ -274,7 +274,7 @@ Thus, example dataset entries could look like this:
     - {project: EMAC, dataset: EMAC, exp: historical, mip: Amon, short_name: ta, raw_name: tm1_p39_cav, start_year: 2000, end_year: 2014}
 
 Please note the duplication of the name ``EMAC`` in ``project`` and
-``dataset``, which is necessary to comply with ESMValTool's data finding and
+``dataset``, which is necessary to comply with ESMValCore's data finding and
 CMORizing functionalities.
 A variable-specific default for the facet ``channel`` is given in the extra
 facets (see next paragraph) for many variables, but this can be overwritten in
@@ -285,7 +285,7 @@ facets<extra_facets>`.
 By default, the file :download:`emac-mappings.yml
 </../esmvalcore/config/extra_facets/emac-mappings.yml>` is used for that
 purpose.
-For some variables, extra facets are necessary; otherwise ESMValTool cannot
+For some variables, extra facets are necessary; otherwise ESMValCore cannot
 read them properly.
 Supported keys for extra facets are:
 
@@ -326,7 +326,7 @@ Key                  Description                            Default value if not
 ICON
 ^^^^
 
-ESMValTool is able to read native `ICON
+ESMValCore is able to read native `ICON
 <https://code.mpimet.mpg.de/projects/iconpublic>`_ model output.
 
 The default naming conventions for input directories and files for ICON are
@@ -349,7 +349,7 @@ Thus, example dataset entries could look like this:
        end_year: 2014}
 
 Please note the duplication of the name ``ICON`` in ``project`` and
-``dataset``, which is necessary to comply with ESMValTool's data finding and
+``dataset``, which is necessary to comply with ESMValCore's data finding and
 CMORizing functionalities.
 A variable-specific default for the facet ``var_type`` is given in the extra
 facets (see below) for many variables, but this can be overwritten in the
@@ -460,7 +460,7 @@ facets<extra_facets>`.
 By default, the file :download:`icon-mappings.yml
 </../esmvalcore/config/extra_facets/icon-mappings.yml>` is used for that
 purpose.
-For some variables, extra facets are necessary; otherwise ESMValTool cannot
+For some variables, extra facets are necessary; otherwise ESMValCore cannot
 read them properly.
 Supported keys for extra facets are:
 
@@ -569,7 +569,7 @@ files must also undergo some data selection.
 
 Data retrieval
 ==============
-Data retrieval in ESMValTool has two main aspects from the user's point of
+Data retrieval in ESMValCore has two main aspects from the user's point of
 view:
 
 * data can be found by the tool, subject to availability on disk or `ESGF <https://esgf.llnl.gov/>`_;
@@ -577,7 +577,7 @@ view:
 
 The first point is self-explanatory: if the user runs the tool on a machine
 that has access to a data repository or multiple data repositories, then
-ESMValTool will look for and find the available data requested by the user.
+ESMValCore will look for and find the available data requested by the user.
 If the files are not found locally, the tool can search the ESGF_ and download
 the missing files, provided that they are available.
 
@@ -598,7 +598,7 @@ the :ref:`user configuration file`.
 
 Setting the correct root paths
 ------------------------------
-The first step towards providing ESMValTool the correct set of parameters for
+The first step towards providing ESMValCore the correct set of parameters for
 data retrieval is setting the root paths to the data. This is done in the user
 configuration file ``config-user.yml``. The two sections where the user will
 set the paths are ``rootpath`` and ``drs``. ``rootpath`` contains pointers to
@@ -608,24 +608,11 @@ first discuss the ``drs`` parameter: as we've seen in the previous section, the
 DRS as a standard is used for both file naming conventions and for directory
 structures.
 
-Synda
------
-
-If the `synda install <https://prodiguer.github.io/synda/sdt/user_guide.html#synda-install>`_ command is used to download data,
-it maintains the directory structure as on ESGF. To find data downloaded by
-synda, use the ``SYNDA`` ``drs`` parameter.
-
-.. code-block:: yaml
-
- drs:
-   CMIP6: SYNDA
-   CMIP5: SYNDA
-
 .. _config-user-drs:
 
 Explaining ``config-user/drs: CMIP5:`` or ``config-user/drs: CMIP6:``
 ---------------------------------------------------------------------
-Whereas ESMValTool will **always** use the CMOR standard for file naming (please
+Whereas ESMValCore will by default use the CMOR standard for file naming (please
 refer above), by setting the ``drs`` parameter the user tells the tool what
 type of root paths they need the data from, e.g.:
 
@@ -655,10 +642,17 @@ is another way to retrieve data from a ``ROOT`` directory that has no DRS-like
 structure; ``default`` indicates that the data lies in a directory that
 contains all the files without any structure.
 
+The names of the directories trees that can be used under `drs` are defined in
+:ref:`config-developer`.
+
 .. note::
-   When using ``CMIP6: default`` or ``CMIP5: default`` it is important to
-   remember that all the needed files must be in the same top-level directory
-   set by ``default`` (see below how to set ``default``).
+   When using ``CMIP6: default`` or ``CMIP5: default``, all the needed files
+   must be in the same top-level directory specified under ``rootpath``.
+   However, it is not recommended to use this, as it makes it impossible for
+   the tool to read the facets from the directory tree.
+   Moreover, this way of organizing data makes it impossible to store multiple
+   versions of the same file because the files typically have the same name
+   for different versions.
 
 .. _config-user-rootpath:
 
@@ -668,27 +662,37 @@ Explaining ``config-user/rootpath:``
 ``rootpath`` identifies the root directory for different data types (``ROOT`` as we used it above):
 
 * ``CMIP`` e.g. ``CMIP5`` or ``CMIP6``: this is the `root` path(s) to where the
-  CMIP files are stored; it can be a single path or a list of paths; it can
+  CMIP files are stored; it can be a single path, a list of paths, or a mapping
+  with paths as keys and `drs` names as values; it can
   point to an ESGF node or it can point to a user private repository. Example
-  for a CMIP5 root path pointing to the ESGF node on CEDA-Jasmin (formerly
+  for a CMIP5 root path pointing to the ESGF node mounted on CEDA-Jasmin (formerly
   known as BADC):
 
   .. code-block:: yaml
 
-    CMIP5: /badc/cmip5/data/cmip5/output1
+    rootpath:
+      CMIP5: /badc/cmip5/data/cmip5/output1
 
   Example for a CMIP6 root path pointing to the ESGF node on CEDA-Jasmin:
 
   .. code-block:: yaml
 
-    CMIP6: /badc/cmip6/data/CMIP6/CMIP
+    rootpath:
+      CMIP6: /badc/cmip6/data/CMIP6
 
   Example for a mix of CMIP6 root path pointing to the ESGF node on CEDA-Jasmin
   and a user-specific data repository for extra data:
 
   .. code-block:: yaml
 
-    CMIP6: [/badc/cmip6/data/CMIP6/CMIP, /home/users/johndoe/cmip_data]
+    rootpath:
+      CMIP6:
+        /badc/cmip6/data/CMIP6: BADC
+        ~/climate_data: ESGF
+
+  Note that this notation combines the ``rootpath`` and ``drs`` settings, so it
+  is not necessary to specify the directory structure in under ``drs`` in this
+  case.
 
 * ``OBS``: this is the `root` path(s) to where the observational datasets are
   stored; again, this could be a single path or a list of paths, just like for
@@ -697,7 +701,8 @@ Explaining ``config-user/rootpath:``
 
   .. code-block:: yaml
 
-    OBS: /gws/nopw/j04/esmeval/obsdata-v2
+    rootpath:
+      OBS: /gws/nopw/j04/esmeval/obsdata-v2
 
 * ``default``: this is the `root` path(s) where the tool will look for data
   from projects that do not have their own rootpath set.
@@ -705,9 +710,22 @@ Explaining ``config-user/rootpath:``
 * ``RAWOBS``: this is the `root` path(s) to where the raw observational data
   files are stored; this is used by ``esmvaltool data format``.
 
+Synda
+-----
+
+If the `synda install <https://prodiguer.github.io/synda/sdt/user_guide.html#synda-install>`_ command is used to download data,
+it maintains the directory structure as on ESGF. To find data downloaded by
+synda, use the ``SYNDA`` ``drs`` parameter.
+
+.. code-block:: yaml
+
+ drs:
+   CMIP6: SYNDA
+   CMIP5: SYNDA
+
 Dataset definitions in ``recipe``
 ---------------------------------
-Once the correct paths have been established, ESMValTool collects the
+Once the correct paths have been established, ESMValCore collects the
 information on the specific datasets that are needed for the analysis. This
 information, together with the CMOR convention for naming files (see CMOR-DRS_)
 will allow the tool to search and find the right files. The specific
diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst
index 74229e08e..dd5e61b36 100644
--- a/doc/recipe/preprocessor.rst
+++ b/doc/recipe/preprocessor.rst
@@ -97,7 +97,7 @@ supported too if proper keyword arguments are specified:
 ``hmean``                      :const:`iris.analysis.HMEAN`                      no
 ``max``                        :const:`iris.analysis.MAX`                        no
 ``mean``                       :const:`iris.analysis.MEAN`                       yes
-``median``                     :const:`iris.analysis.MEDIAN` [#f2]_                no
+``median``                     :const:`iris.analysis.MEDIAN` [#f2]_              no
 ``min``                        :const:`iris.analysis.MIN`                        no
 ``peak``                       :const:`iris.analysis.PEAK`                       no
 ``percentile``                 :const:`iris.analysis.PERCENTILE`                 no
@@ -1570,29 +1570,44 @@ resample_hours:
 ``resample_hours``
 ------------------
 
-This function changes the frequency of the data in the cube by extracting the
-timesteps that belongs to the desired frequency. It is important to note that
-it is mainly mean to be used with instantaneous data
+Change the frequency of x-hourly data to y-hourly data by either eliminating
+extra time steps or interpolation.
+This is intended to be used with instantaneous data.
 
 Parameters:
-    * interval: New frequency of the data. Must be a divisor of 24
-    * offset: First desired hour. Default 0. Must be lower than the interval
+    * `interval` (:obj:`int`): New frequency of the data. Must be a divisor of 24.
+    * `offset` (:obj:`int`): First hour of the desired output data (default:
+      0). Must be lower than the value of `interval`.
+    * `interpolate` (``None`` or :obj:`str`): If `interpolate` is ``None``
+      (default), convert x-hourly data to y-hourly (y > x) by eliminating extra
+      time steps. If `interpolate` is 'nearest' or 'linear', use
+      nearest-neighbor or bilinear interpolation to convert general x-hourly
+      data to general y-hourly data.
 
 Examples:
-    * Convert to 12-hourly, by getting timesteps at 0:00 and 12:00:
 
-        .. code-block:: yaml
+* Convert to 12-hourly data by getting time steps at 0:00 and 12:00:
 
-            resample_hours:
-              hours: 12
+    .. code-block:: yaml
 
-    * Convert to 12-hourly, by getting timesteps at 6:00 and 18:00:
+        resample_hours:
+          hours: 12
 
-        .. code-block:: yaml
+* Convert to 12-hourly data by getting time steps at 6:00 and 18:00:
+
+    .. code-block:: yaml
+
+        resample_hours:
+          hours: 12
+          offset: 6
+
+* Convert to 3-hourly data using bilinear interpolation:
+
+    .. code-block:: yaml
 
-            resample_hours:
-              hours: 12
-	      offset: 6
+        resample_hours:
+          hours: 3
+          interpolate: linear
 
 See also :func:`esmvalcore.preprocessor.resample_hours`.
 
diff --git a/environment.yml b/environment.yml
index 0f68f0642..6405b6ca5 100644
--- a/environment.yml
+++ b/environment.yml
@@ -12,15 +12,15 @@ dependencies:
   - dask-jobqueue
   - distributed
   - esgf-pyclient >=0.3.1
-  - esmpy !=8.1.0,<8.6.0  # https://github.com/SciTools-incubator/iris-esmf-regrid/pull/342#issuecomment-2092921514
+  - esmpy >=8.6.0  # github.com/SciTools-incubator/iris-esmf-regrid/pull/342
   - filelock
   - fiona
   - fire
   - geopy
   - humanfriendly
   - importlib_metadata  # required for Python < 3.10
-  - iris >3.8.0
-  - iris-esmf-regrid >=0.7.0
+  - iris >=3.9.0
+  - iris-esmf-regrid >=0.10.0  # github.com/SciTools-incubator/iris-esmf-regrid/pull/342
   - isodate
   - jinja2
   - libnetcdf !=4.9.1  # to avoid hdf5 warnings
diff --git a/esmvalcore/_recipe/check.py b/esmvalcore/_recipe/check.py
index 940a578ae..ac7b7f5be 100644
--- a/esmvalcore/_recipe/check.py
+++ b/esmvalcore/_recipe/check.py
@@ -409,8 +409,8 @@ def _check_literal(
     user_value = settings[step].get(option, allowed_values[0])
     if user_value not in allowed_values:
         raise RecipeError(
-            f"Expected one of {allowed_values} for `{option}`, got "
-            f"'{user_value}'"
+            f"Expected one of {allowed_values} for option `{option}` of "
+            f"preprocessor `{step}`, got '{user_value}'"
         )
 
 
@@ -437,6 +437,14 @@ metric_type = partial(
 )
 
 
+resample_hours = partial(
+    _check_literal,
+    step='resample_hours',
+    option='interpolate',
+    allowed_values=(None, 'nearest', 'linear'),
+)
+
+
 def _check_ref_attributes(products: set, *, step: str, attr_name: str) -> None:
     """Check that exactly one reference dataset is given."""
     products = {p for p in products if step in p.settings}
diff --git a/esmvalcore/_recipe/recipe.py b/esmvalcore/_recipe/recipe.py
index 676e56cb5..1f2eb6348 100644
--- a/esmvalcore/_recipe/recipe.py
+++ b/esmvalcore/_recipe/recipe.py
@@ -658,6 +658,7 @@ def _update_preproc_functions(settings, dataset, datasets, missing_vars):
     check.regridding_schemes(settings)
     check.bias_type(settings)
     check.metric_type(settings)
+    check.resample_hours(settings)
 
 
 def _get_preprocessor_task(datasets, profiles, task_name):
diff --git a/esmvalcore/cmor/_fixes/icon/_base_fixes.py b/esmvalcore/cmor/_fixes/icon/_base_fixes.py
index e4196b035..9c242ef73 100644
--- a/esmvalcore/cmor/_fixes/icon/_base_fixes.py
+++ b/esmvalcore/cmor/_fixes/icon/_base_fixes.py
@@ -19,7 +19,7 @@ from iris.cube import Cube, CubeList
 from iris.experimental.ugrid import Connectivity, Mesh
 
 from esmvalcore.cmor._fixes.native_datasets import NativeDatasetFix
-from esmvalcore.local import _get_rootpath, _replace_tags, _select_drs
+from esmvalcore.local import _get_data_sources
 
 logger = logging.getLogger(__name__)
 
@@ -297,12 +297,12 @@ class IconFix(NativeDatasetFix):
 
     def _get_grid_from_rootpath(self, grid_name: str) -> CubeList | None:
         """Try to get grid from the ICON rootpath."""
-        rootpaths = _get_rootpath('ICON')
-        dirname_template = _select_drs('input_dir', 'ICON')
-        dirname_globs = _replace_tags(dirname_template, self.extra_facets)
-        possible_grid_paths = [
-            r / d / grid_name for r in rootpaths for d in dirname_globs
-        ]
+        glob_patterns: list[Path] = []
+        for data_source in _get_data_sources('ICON'):
+            glob_patterns.extend(
+                data_source.get_glob_patterns(**self.extra_facets)
+            )
+        possible_grid_paths = [d.parent / grid_name for d in glob_patterns]
         for grid_path in possible_grid_paths:
             if grid_path.is_file():
                 logger.debug("Using ICON grid file '%s'", grid_path)
diff --git a/esmvalcore/config-user.yml b/esmvalcore/config-user.yml
index c102928db..ecdee818f 100644
--- a/esmvalcore/config-user.yml
+++ b/esmvalcore/config-user.yml
@@ -109,13 +109,25 @@ drs:
   CORDEX: ESGF
   obs4MIPs: ESGF
 
-# Example rootpaths and directory structure that showcases the different
-# projects and also the use of lists
+# Example rootpaths and directory structure names for different projects.
+# For each project, the entry can be a single path, a list of paths, or a
+# mapping from paths to directory structure names.
+# For single paths and list of paths, the directory structure names can be
+# defined under 'drs'.
+# If no path is defined for a project, the tool will look in the 'default'
+# path.
+# If no directory structure name is given, the name 'default' will be used.
+# Directory structures corresponding to the names are defined in the file
+# config-developer.yml.
 # For site-specific entries, see below.
 #rootpath:
-#  CMIP3: [~/cmip3_inputpath1, ~/cmip3_inputpath2]
-#  CMIP5: [~/cmip5_inputpath1, ~/cmip5_inputpath2]
-#  CMIP6: [~/cmip6_inputpath1, ~/cmip6_inputpath2]
+#  CMIP6:
+#    /path/to/data: DKRZ
+#    ~/path/to/more/data: ESGF
+#  CMIP5:
+#    - ~/cmip5_inputpath1
+#    - ~/cmip5_inputpath2
+#  CMIP3: ~/cmip6_inputpath
 #  OBS: ~/obs_inputpath
 #  OBS6: ~/obs6_inputpath
 #  obs4MIPs: ~/obs4mips_inputpath
@@ -124,11 +136,10 @@ drs:
 #  RAWOBS: ~/rawobs_inputpath
 #  default: ~/default_inputpath
 #drs:
-#  CMIP3: default
-#  CMIP5: default
-#  CMIP6: default
-#  CORDEX: default
-#  obs4MIPs: default
+#  CMIP3: ESGF
+#  CMIP5: ESGF
+#  CORDEX: ESGF
+#  obs4MIPs: ESGF
 
 # Directory tree created by automatically downloading from ESGF
 # Uncomment the lines below to locate data that has been automatically
@@ -175,22 +186,27 @@ drs:
 # Uncomment the lines below to locate data on Levante at DKRZ.
 #auxiliary_data_dir: /work/bd0854/DATA/ESMValTool2/AUX
 #rootpath:
-#  CMIP6: /work/bd0854/DATA/ESMValTool2/CMIP6_DKRZ
-#  CMIP5: /work/bd0854/DATA/ESMValTool2/CMIP5_DKRZ
-#  CMIP3: /work/bd0854/DATA/ESMValTool2/CMIP3
-#  CORDEX: /work/ik1017/C3SCORDEX/data/c3s-cordex/output
+#  CMIP6:
+#    /work/bd0854/DATA/ESMValTool2/CMIP6_DKRZ: DKRZ
+#    /work/bd0854/DATA/ESMValTool2/download: ESGF
+#  CMIP5:
+#    /work/bd0854/DATA/ESMValTool2/CMIP5_DKRZ: DKRZ
+#    /work/bd0854/DATA/ESMValTool2/download: ESGF
+#  CMIP3:
+#    /work/bd0854/DATA/ESMValTool2/CMIP3: DKRZ
+#    /work/bd0854/DATA/ESMValTool2/download: ESGF
+#  CORDEX:
+#    /work/ik1017/C3SCORDEX/data/c3s-cordex/output: BADC
+#    /work/bd0854/DATA/ESMValTool2/download: ESGF
 #  OBS: /work/bd0854/DATA/ESMValTool2/OBS
 #  OBS6: /work/bd0854/DATA/ESMValTool2/OBS
-#  obs4MIPs: /work/bd0854/DATA/ESMValTool2/OBS
+#  obs4MIPs:
+#    /work/bd0854/DATA/ESMValTool2/OBS: default
+#    /work/bd0854/DATA/ESMValTool2/download: ESGF
 #  ana4mips: /work/bd0854/DATA/ESMValTool2/OBS
 #  native6: /work/bd0854/DATA/ESMValTool2/RAWOBS
 #  RAWOBS: /work/bd0854/DATA/ESMValTool2/RAWOBS
 #drs:
-#  CMIP6: DKRZ
-#  CMIP5: DKRZ
-#  CMIP3: DKRZ
-#  CORDEX: BADC
-#  obs4MIPs: default
 #  ana4mips: default
 #  OBS: default
 #  OBS6: default
diff --git a/esmvalcore/config/_config_validators.py b/esmvalcore/config/_config_validators.py
index 867370ae7..d6489862d 100644
--- a/esmvalcore/config/_config_validators.py
+++ b/esmvalcore/config/_config_validators.py
@@ -205,7 +205,17 @@ def validate_rootpath(value):
                 "Correcting capitalization, project 'obs4mips' should be "
                 "written as 'obs4MIPs' in 'rootpath' in config-user.yml")
             key = 'obs4MIPs'
-        new_mapping[key] = validate_pathlist(paths)
+        if isinstance(paths, Path):
+            paths = str(paths)
+        if isinstance(paths, (str, list)):
+            new_mapping[key] = validate_pathlist(paths)
+        else:
+            validate_dict(paths)
+            new_mapping[key] = {
+                validate_path(path): validate_string(drs)
+                for path, drs in paths.items()
+            }
+
     return new_mapping
 
 
diff --git a/esmvalcore/local.py b/esmvalcore/local.py
index 5cb499dbb..539679f68 100644
--- a/esmvalcore/local.py
+++ b/esmvalcore/local.py
@@ -5,6 +5,7 @@ import itertools
 import logging
 import os
 import re
+from dataclasses import dataclass
 from glob import glob
 from pathlib import Path
 from typing import TYPE_CHECKING, Any, Union
@@ -376,78 +377,88 @@ def _apply_caps(original, lower, upper):
     return original
 
 
-def _select_drs(input_type, project):
+def _select_drs(input_type: str, project: str, structure: str) -> list[str]:
     """Select the directory structure of input path."""
     cfg = get_project_config(project)
-    input_path = cfg[input_type]
-    if isinstance(input_path, str):
-        return input_path
+    input_path_patterns = cfg[input_type]
+    if isinstance(input_path_patterns, str):
+        return [input_path_patterns]
 
-    structure = CFG['drs'].get(project, 'default')
-    if structure in input_path:
-        return input_path[structure]
+    if structure in input_path_patterns:
+        value = input_path_patterns[structure]
+        if isinstance(value, str):
+            value = [value]
+        return value
 
     raise KeyError(
         'drs {} for {} project not specified in config-developer file'.format(
             structure, project))
 
 
-_ROOTPATH_WARNED = set()
+@dataclass(order=True, frozen=True)
+class DataSource:
+    """Class for storing a data source and finding the associated files."""
 
+    rootpath: Path
+    dirname_template: str
+    filename_template: str
 
-def _get_rootpath(project):
-    """Select the rootpath."""
-    rootpath = CFG['rootpath']
-    for key in (project, 'default'):
-        if key in rootpath:
-            nonexistent = tuple(p for p in rootpath[key]
-                                if not os.path.exists(p))
-            if nonexistent and (key, nonexistent) not in _ROOTPATH_WARNED:
-                logger.warning(
-                    "'%s' rootpaths '%s' set in config-user.yml do not exist",
-                    key, ', '.join(str(p) for p in nonexistent))
-                _ROOTPATH_WARNED.add((key, nonexistent))
-            return rootpath[key]
-    raise KeyError(f'The "{project}" option is missing from the "rootpath" '
-                   'section in the config-user.yml file.')
+    def get_glob_patterns(self, **facets) -> list[Path]:
+        """Compose the globs that will be used to look for files."""
+        dirname_globs = _replace_tags(self.dirname_template, facets)
+        filename_globs = _replace_tags(self.filename_template, facets)
+        return sorted(self.rootpath / d / f for d in dirname_globs
+                      for f in filename_globs)
 
+    def find_files(self, **facets) -> list[LocalFile]:
+        """Find files."""
+        globs = self.get_glob_patterns(**facets)
+        logger.debug("Looking for files matching %s", globs)
 
-def _get_globs(variable):
-    """Compose the globs that will be used to look for files."""
-    project = variable['project']
+        files = []
+        for glob_ in globs:
+            for filename in glob(str(glob_)):
+                file = LocalFile(filename)
+                file.facets.update(_path2facets(file, self.dirname_template))
+                files.append(file)
+        files.sort()  # sorting makes it easier to see what was found
 
-    rootpaths = _get_rootpath(project)
+        if 'timerange' in facets:
+            files = _select_files(files, facets['timerange'])
+        return files
 
-    dirname_template = _select_drs('input_dir', project)
-    dirname_globs = _replace_tags(dirname_template, variable)
 
-    filename_template = _select_drs('input_file', project)
-    filename_globs = _replace_tags(filename_template, variable)
-
-    globs = sorted(r / d / f for r in rootpaths for d in dirname_globs
-                   for f in filename_globs)
-    return globs
-
-
-def _get_input_filelist(variable):
-    """Return the full path to input files."""
-    variable = dict(variable)
-    if 'original_short_name' in variable:
-        variable['short_name'] = variable['original_short_name']
-
-    globs = _get_globs(variable)
-    logger.debug(
-        "Looking for files matching:\n%s",
-        "\n".join(str(g) for g in globs),
-    )
+_ROOTPATH_WARNED = set()
 
-    files = list(Path(file) for glob_ in globs for file in glob(str(glob_)))
-    files.sort()  # sorting makes it easier to see what was found
 
-    if 'timerange' in variable:
-        files = _select_files(files, variable['timerange'])
+def _get_data_sources(project: str) -> list[DataSource]:
+    """Get a list of data sources."""
+    rootpaths = CFG['rootpath']
+    for key in (project, 'default'):
+        if key in rootpaths:
+            paths = rootpaths[key]
+            nonexistent = tuple(p for p in paths if not os.path.exists(p))
+            if nonexistent and (key, nonexistent) not in _ROOTPATH_WARNED:
+                logger.warning(
+                    "'%s' rootpaths '%s' set in config-user.yml do not exist",
+                    key, ', '.join(str(p) for p in nonexistent))
+                _ROOTPATH_WARNED.add((key, nonexistent))
+            if isinstance(paths, list):
+                structure = CFG['drs'].get(project, 'default')
+                paths = {p: structure for p in paths}
+            sources: list[DataSource] = []
+            for path, structure in paths.items():
+                dir_templates = _select_drs('input_dir', project, structure)
+                file_templates = _select_drs('input_file', project, structure)
+                sources.extend(
+                    DataSource(path, d, f)
+                    for d in dir_templates for f in file_templates
+                )
+            return sources
 
-    return files, globs
+    raise KeyError(
+        f"No '{project}' or 'default' path specified under 'rootpath' in "
+        "the user configuration.")
 
 
 def _get_output_file(variable: dict[str, Any], preproc_dir: Path) -> Path:
@@ -626,19 +637,18 @@ def find_files(
     list[LocalFile]
         The files that were found.
     """  # pylint: disable=line-too-long
-    filenames, globs = _get_input_filelist(facets)
-    drs = _select_drs('input_dir', facets['project'])
-    if isinstance(drs, list):
-        # Not sure how to handle a list of DRSs
-        drs = ''
+    facets = dict(facets)
+    if 'original_short_name' in facets:
+        facets['short_name'] = facets['original_short_name']
+
     files = []
     filter_latest = False
-    for filename in filenames:
-        file = LocalFile(filename)
-        file.facets.update(_path2facets(file, drs))
-        if file.facets.get('version') == 'latest':
-            filter_latest = True
-        files.append(file)
+    data_sources = _get_data_sources(facets['project'])  # type: ignore
+    for data_source in data_sources:
+        for file in data_source.find_files(**facets):
+            if file.facets.get('version') == 'latest':
+                filter_latest = True
+            files.append(file)
 
     if filter_latest:
         files = _filter_versions_called_latest(files)
@@ -646,8 +656,13 @@ def find_files(
     if 'version' not in facets:
         files = _select_latest_version(files)
 
+    files.sort()  # sorting makes it easier to see what was found
+
     if debug:
-        return files, globs
+        globs = []
+        for data_source in data_sources:
+            globs.extend(data_source.get_glob_patterns(**facets))
+        return files, sorted(globs)
     return files
 
 
diff --git a/esmvalcore/preprocessor/_mask.py b/esmvalcore/preprocessor/_mask.py
index c0e8348d7..cdedcc639 100644
--- a/esmvalcore/preprocessor/_mask.py
+++ b/esmvalcore/preprocessor/_mask.py
@@ -4,6 +4,7 @@ Module that performs a number of masking operations that include:
 masking with ancillary variables, masking with Natural Earth shapefiles
 (land or ocean), masking on thresholds, missing values masking.
 """
+from __future__ import annotations
 
 import logging
 import os
@@ -323,7 +324,14 @@ def _mask_with_shp(cube, shapefilename, region_indices=None):
     return cube
 
 
-def count_spells(data, threshold, axis, spell_length):
+def count_spells(
+    data: np.ndarray | da.Array,
+    threshold: float | None,
+    axis: int,
+    spell_length,
+) -> np.ndarray | da.Array:
+    # Copied from:
+    # https://scitools-iris.readthedocs.io/en/stable/generated/gallery/general/plot_custom_aggregation.html
     """Count data occurrences.
 
     Define a function to perform the custom statistical operation.
@@ -338,10 +346,10 @@ def count_spells(data, threshold, axis, spell_length):
 
     Parameters
     ----------
-    data: ndarray
+    data:
         raw data to be compared with value threshold.
 
-    threshold: float
+    threshold:
         threshold point for 'significant' datapoints.
 
     axis: int
@@ -353,15 +361,17 @@ def count_spells(data, threshold, axis, spell_length):
 
     Returns
     -------
-    int
+    :obj:`numpy.ndarray` or :obj:`dask.array.Array`
         Number of counts.
     """
     if axis < 0:
         # just cope with negative axis numbers
         axis += data.ndim
     # Threshold the data to find the 'significant' points.
+    array_module = da if isinstance(data, da.Array) else np
     if not threshold:
-        data_hits = np.ones_like(data, dtype=bool)
+        # Keeps the mask of the input data.
+        data_hits = array_module.ma.ones_like(data, dtype=bool)
     else:
         data_hits = data > float(threshold)
 
@@ -371,17 +381,16 @@ def count_spells(data, threshold, axis, spell_length):
     # if you want overlapping windows set the step to be m*spell_length
     # where m is a float
     ###############################################################
-    hit_windows = rolling_window(data_hits,
-                                 window=spell_length,
-                                 step=spell_length,
-                                 axis=axis)
-
+    hit_windows = rolling_window(
+        data_hits,
+        window=spell_length,
+        step=spell_length,
+        axis=axis,
+    )
     # Find the windows "full of True-s" (along the added 'window axis').
-    full_windows = np.all(hit_windows, axis=axis + 1)
-
+    full_windows = array_module.all(hit_windows, axis=axis + 1)
     # Count points fulfilling the condition (along the time axis).
-    spell_point_counts = np.sum(full_windows, axis=axis, dtype=int)
-
+    spell_point_counts = array_module.sum(full_windows, axis=axis, dtype=int)
     return spell_point_counts
 
 
@@ -572,10 +581,12 @@ def mask_multimodel(products):
         f"got {product_types}")
 
 
-def mask_fillvalues(products,
-                    threshold_fraction,
-                    min_value=None,
-                    time_window=1):
+def mask_fillvalues(
+    products,
+    threshold_fraction: float,
+    min_value: float | None = None,
+    time_window: int = 1,
+):
     """Compute and apply a multi-dataset fillvalues mask.
 
     Construct the mask that fills a certain time window with missing values
@@ -590,15 +601,15 @@ def mask_fillvalues(products,
     products: iris.cube.Cube
         data products to be masked.
 
-    threshold_fraction: float
+    threshold_fraction:
         fractional threshold to be used as argument for Aggregator.
         Must be between 0 and 1.
 
-    min_value: float
+    min_value:
         minimum value threshold; default None
         If default, no thresholding applied so the full mask will be selected.
 
-    time_window: float
+    time_window:
         time window to compute missing data counts; default set to 1.
 
     Returns
@@ -611,48 +622,58 @@ def mask_fillvalues(products,
     NotImplementedError
         Implementation missing for data with higher dimensionality than 4.
     """
-    combined_mask = None
+    array_module = da if any(c.has_lazy_data() for p in products
+                             for c in p.cubes) else np
 
-    logger.debug("Creating fillvalues mask")
-    used = set()
+    combined_mask = None
     for product in products:
-        for cube in product.cubes:
-            cube.data = np.ma.fix_invalid(cube.data, copy=False)
-            mask = _get_fillvalues_mask(cube, threshold_fraction, min_value,
-                                        time_window)
+        for i, cube in enumerate(product.cubes):
+            cube = cube.copy()
+            product.cubes[i] = cube
+            cube.data = array_module.ma.fix_invalid(cube.core_data())
+            mask = _get_fillvalues_mask(
+                cube,
+                threshold_fraction,
+                min_value,
+                time_window,
+            )
             if combined_mask is None:
-                combined_mask = np.zeros_like(mask)
+                combined_mask = array_module.zeros_like(mask)
             # Select only valid (not all masked) pressure levels
-            n_dims = len(mask.shape)
-            if n_dims == 2:
-                valid = ~np.all(mask)
-                if valid:
-                    combined_mask |= mask
-                    used.add(product)
-            elif n_dims == 3:
-                valid = ~np.all(mask, axis=(1, 2))
-                combined_mask[valid] |= mask[valid]
-                if np.any(valid):
-                    used.add(product)
+            if mask.ndim in (2, 3):
+                valid = ~mask.all(axis=(-2, -1), keepdims=True)
             else:
                 raise NotImplementedError(
-                    f"Unable to handle {n_dims} dimensional data"
+                    f"Unable to handle {mask.ndim} dimensional data"
                 )
+            combined_mask = array_module.where(
+                valid,
+                combined_mask | mask,
+                combined_mask,
+            )
 
-    if np.any(combined_mask):
-        logger.debug("Applying fillvalues mask")
-        used = {p.copy_provenance() for p in used}
-        for product in products:
-            for cube in product.cubes:
-                cube.data.mask |= combined_mask
-            for other in used:
-                if other.filename != product.filename:
-                    product.wasderivedfrom(other)
+    for product in products:
+        for cube in product.cubes:
+            array = cube.core_data()
+            data = array_module.ma.getdata(array)
+            mask = array_module.ma.getmaskarray(array) | combined_mask
+            cube.data = array_module.ma.masked_array(data, mask)
+
+    # Record provenance
+    input_products = {p.copy_provenance() for p in products}
+    for other in input_products:
+        if other.filename != product.filename:
+            product.wasderivedfrom(other)
 
     return products
 
 
-def _get_fillvalues_mask(cube, threshold_fraction, min_value, time_window):
+def _get_fillvalues_mask(
+    cube: iris.cube.Cube,
+    threshold_fraction: float,
+    min_value: float | None,
+    time_window: int,
+) -> np.ndarray | da.Array:
     """Compute the per-model missing values mask.
 
     Construct the mask that fills a certain time window with missing
@@ -662,7 +683,6 @@ def _get_fillvalues_mask(cube, threshold_fraction, min_value, time_window):
     counts the number of valid (unmasked) data points within that
     window; a simple value thresholding is also applied if needed.
     """
-    # basic checks
     if threshold_fraction < 0 or threshold_fraction > 1.0:
         raise ValueError(
             f"Fraction of missing values {threshold_fraction} should be "
@@ -678,19 +698,24 @@ def _get_fillvalues_mask(cube, threshold_fraction, min_value, time_window):
     counts_threshold = int(max_counts_per_time_window * threshold_fraction)
 
     # Make an aggregator
-    spell_count = Aggregator('spell_count',
-                             count_spells,
-                             units_func=lambda units: 1)
+    spell_count = Aggregator(
+        'spell_count',
+        count_spells,
+        lazy_func=count_spells,
+        units_func=lambda units: 1,
+    )
 
     # Calculate the statistic.
-    counts_windowed_cube = cube.collapsed('time',
-                                          spell_count,
-                                          threshold=min_value,
-                                          spell_length=time_window)
+    counts_windowed_cube = cube.collapsed(
+        'time',
+        spell_count,
+        threshold=min_value,
+        spell_length=time_window,
+    )
 
     # Create mask
-    mask = counts_windowed_cube.data < counts_threshold
-    if np.ma.isMaskedArray(mask):
-        mask = mask.data | mask.mask
+    mask = counts_windowed_cube.core_data() < counts_threshold
+    array_module = da if isinstance(mask, da.Array) else np
+    mask = array_module.ma.filled(mask, True)
 
     return mask
diff --git a/esmvalcore/preprocessor/_time.py b/esmvalcore/preprocessor/_time.py
index b89cc92b0..d4dff4c1f 100644
--- a/esmvalcore/preprocessor/_time.py
+++ b/esmvalcore/preprocessor/_time.py
@@ -10,17 +10,19 @@ import datetime
 import logging
 import warnings
 from functools import partial
-from typing import Iterable, Optional
+from typing import Iterable, Literal, Optional
 from warnings import filterwarnings
 
 import dask.array as da
 import dask.config
 import iris
+import iris.analysis
 import iris.coord_categorisation
 import iris.util
 import isodate
 import numpy as np
 from cf_units import Unit
+from cftime import datetime as cf_datetime
 from iris.coords import AuxCoord, Coord, DimCoord
 from iris.cube import Cube, CubeList
 from iris.exceptions import CoordinateMultiDimError, CoordinateNotFoundError
@@ -1249,21 +1251,23 @@ def timeseries_filter(
     return cube
 
 
-def resample_hours(cube: Cube, interval: int, offset: int = 0) -> Cube:
-    """Convert x-hourly data to y-hourly by eliminating extra timesteps.
+def resample_hours(
+    cube: Cube,
+    interval: int,
+    offset: int = 0,
+    interpolate: Optional[Literal['nearest', 'linear']] = None,
+) -> Cube:
+    """Convert x-hourly data to y-hourly data.
 
-    Convert x-hourly data to y-hourly (y > x) by eliminating the extra
-    timesteps. This is intended to be used only with instantaneous values.
+    This is intended to be used with instantaneous data.
 
-    For example:
-
-    - resample_hours(cube, interval=6): Six-hourly intervals at 0:00, 6:00,
+    Examples
+    --------
+    - ``resample_hours(cube, interval=6)``: Six-hourly intervals at 0:00, 6:00,
       12:00, 18:00.
-
-    - resample_hours(cube, interval=6, offset=3): Six-hourly intervals at
+    - ``resample_hours(cube, interval=6, offset=3)``: Six-hourly intervals at
       3:00, 9:00, 15:00, 21:00.
-
-    - resample_hours(cube, interval=12, offset=6): Twelve-hourly intervals
+    - ``resample_hours(cube, interval=12, offset=6)``: Twelve-hourly intervals
       at 6:00, 18:00.
 
     Parameters
@@ -1271,9 +1275,14 @@ def resample_hours(cube: Cube, interval: int, offset: int = 0) -> Cube:
     cube:
         Input cube.
     interval:
-        The period (hours) of the desired data.
-    offset: optional
-        The firs hour (hours) of the desired data.
+        The period (hours) of the desired output data.
+    offset:
+        The first hour of the desired output data.
+    interpolate:
+        If `interpolate` is ``None`` (default), convert x-hourly data to
+        y-hourly (y > x) by eliminating extra time steps. If `interpolate` is
+        'nearest' or 'linear', use nearest-neighbor or bilinear interpolation
+        to convert general x-hourly data to general y-hourly data.
 
     Returns
     -------
@@ -1283,7 +1292,9 @@ def resample_hours(cube: Cube, interval: int, offset: int = 0) -> Cube:
     Raises
     ------
     ValueError:
-        The specified frequency is not a divisor of 24.
+        `interval` is not a divisor of 24; invalid `interpolate` given; or
+        input data does not contain any target hour (if `interpolate` is
+        ``None``).
 
     """
     allowed_intervals = (1, 2, 3, 4, 6, 12)
@@ -1298,15 +1309,38 @@ def resample_hours(cube: Cube, interval: int, offset: int = 0) -> Cube:
     if cube_period.total_seconds() / 3600 > interval:
         raise ValueError(f"Data period ({cube_period}) should be lower than "
                          f"the interval ({interval})")
-    hours = [PartialDateTime(hour=h) for h in range(0 + offset, 24, interval)]
     dates = time.units.num2date(time.points)
-    select = np.zeros(len(dates), dtype=bool)
-    for hour in hours:
-        select |= dates == hour
-    cube = _select_timeslice(cube, select)
-    if cube is None:
-        raise ValueError(
-            f"Time coordinate {dates} does not contain {hours} for {cube}")
+
+    # Interpolate input time to requested hours if desired
+    if interpolate:
+        if interpolate == 'nearest':
+            interpolation_scheme = iris.analysis.Nearest()
+        elif interpolate == 'linear':
+            interpolation_scheme = iris.analysis.Linear()
+        else:
+            raise ValueError(
+                f"Expected `None`, 'nearest' or 'linear' for `interpolate`, "
+                f"got '{interpolate}'"
+            )
+        new_dates = sorted([
+            cf_datetime(y, m, d, h, calendar=time.units.calendar)
+            for h in range(0 + offset, 24, interval)
+            for (y, m, d) in {(d.year, d.month, d.day) for d in dates}
+        ])
+        cube = cube.interpolate([('time', new_dates)], interpolation_scheme)
+    else:
+        hours = [
+            PartialDateTime(hour=h) for h in range(0 + offset, 24, interval)
+        ]
+        dates = time.units.num2date(time.points)
+        select = np.zeros(len(dates), dtype=bool)
+        for hour in hours:
+            select |= dates == hour
+        cube = _select_timeslice(cube, select)
+        if cube is None:
+            raise ValueError(
+                f"Time coordinate {dates} does not contain {hours} for {cube}"
+            )
 
     return cube
 
diff --git a/esmvalcore/preprocessor/_volume.py b/esmvalcore/preprocessor/_volume.py
index 3d04f2350..c59fe8293 100644
--- a/esmvalcore/preprocessor/_volume.py
+++ b/esmvalcore/preprocessor/_volume.py
@@ -16,6 +16,7 @@ from iris.coords import AuxCoord, CellMeasure
 from iris.cube import Cube
 
 from ._shared import (
+    broadcast_to_shape,
     get_iris_aggregator,
     get_normalized_cube,
     preserve_float_dtype,
@@ -152,6 +153,8 @@ def calculate_volume(cube: Cube) -> da.core.Array:
 
     # Calculate Z-direction thickness
     thickness = depth.core_bounds()[..., 1] - depth.core_bounds()[..., 0]
+    if cube.has_lazy_data():
+        thickness = da.array(thickness)
 
     # Get or calculate the horizontal areas of the cube
     has_cell_measure = bool(cube.cell_measures('cell_area'))
@@ -166,10 +169,11 @@ def calculate_volume(cube: Cube) -> da.core.Array:
     if not has_cell_measure:
         cube.remove_cell_measure('cell_area')
 
-    area_arr = iris.util.broadcast_to_shape(
-        area.core_data(), cube.shape, area_dim)
-    thickness_arr = iris.util.broadcast_to_shape(
-        thickness, cube.shape, z_dim)
+    chunks = cube.core_data().chunks if cube.has_lazy_data() else None
+    area_arr = broadcast_to_shape(
+        area.core_data(), cube.shape, area_dim, chunks=chunks)
+    thickness_arr = broadcast_to_shape(
+        thickness, cube.shape, z_dim, chunks=chunks)
     grid_volume = area_arr * thickness_arr
 
     return grid_volume
diff --git a/setup.py b/setup.py
index c969fa8f6..943820276 100755
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@ REQUIREMENTS = {
         'dask[array,distributed]',
         'dask-jobqueue',
         'esgf-pyclient>=0.3.1',
-        'esmf-regrid',
+        'esmf-regrid>=0.10.0',  # iris-esmf-regrid #342
         'esmpy!=8.1.0',  # not on PyPI
         'filelock',
         'fiona',
@@ -56,9 +56,7 @@ REQUIREMENTS = {
         'pyyaml',
         'requests',
         'scipy>=1.6',
-        # See the following issue for info on the iris pin below:
-        # https://github.com/ESMValGroup/ESMValCore/issues/2407
-        'scitools-iris>3.8.0',
+        'scitools-iris>=3.9.0',
         'shapely>=2.0.0',
         'stratify>=0.3',
         'yamale',


Test patch:
diff --git a/tests/integration/cmor/_fixes/icon/test_icon.py b/tests/integration/cmor/_fixes/icon/test_icon.py
index 24fd1d4ba..b1607d07b 100644
--- a/tests/integration/cmor/_fixes/icon/test_icon.py
+++ b/tests/integration/cmor/_fixes/icon/test_icon.py
@@ -516,6 +516,7 @@ def test_get_areacella_fix():
 def test_areacella_fix(cubes_grid):
     """Test fix."""
     fix = get_allvars_fix('fx', 'areacella')
+    fix.extra_facets['var_type'] = 'fx'
     fixed_cubes = fix.fix_metadata(cubes_grid)
 
     assert len(fixed_cubes) == 1
@@ -538,6 +539,7 @@ def test_get_areacello_fix():
 def test_areacello_fix(cubes_grid):
     """Test fix."""
     fix = get_allvars_fix('Ofx', 'areacello')
+    fix.extra_facets['var_type'] = 'fx'
     fixed_cubes = fix.fix_metadata(cubes_grid)
 
     assert len(fixed_cubes) == 1
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 4bc1f406e..fbddec7fe 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -19,7 +19,7 @@ from esmvalcore.local import (
 def session(tmp_path: Path, monkeypatch):
     CFG.clear()
     CFG.update(CFG_DEFAULT)
-    monkeypatch.setitem(CFG, 'rootpath', {'default': str(tmp_path)})
+    monkeypatch.setitem(CFG, 'rootpath', {'default': {tmp_path: 'default'}})
 
     session = CFG.start_session('recipe_test')
     session['output_dir'] = tmp_path / 'esmvaltool_output'
@@ -56,8 +56,8 @@ def _get_files(root_path, facets, tracking_id):
         all_facets = [facets]
 
     # Globs without expanded facets
-    dir_template = _select_drs('input_dir', facets['project'])
-    file_template = _select_drs('input_file', facets['project'])
+    dir_template = _select_drs('input_dir', facets['project'], 'default')
+    file_template = _select_drs('input_file', facets['project'], 'default')
     dir_globs = _replace_tags(dir_template, facets)
     file_globs = _replace_tags(file_template, facets)
     globs = sorted(
@@ -67,8 +67,12 @@ def _get_files(root_path, facets, tracking_id):
     files = []
     for expanded_facets in all_facets:
         filenames = []
-        dir_template = _select_drs('input_dir', expanded_facets['project'])
-        file_template = _select_drs('input_file', expanded_facets['project'])
+        dir_template = _select_drs(
+            'input_dir', expanded_facets['project'], 'default'
+        )
+        file_template = _select_drs(
+            'input_file', expanded_facets['project'], 'default'
+        )
         dir_globs = _replace_tags(dir_template, expanded_facets)
         file_globs = _replace_tags(file_template, expanded_facets)
         filename = (
diff --git a/tests/integration/preprocessor/_mask/test_mask.py b/tests/integration/preprocessor/_mask/test_mask.py
index 298dc3fb2..4e5e51167 100644
--- a/tests/integration/preprocessor/_mask/test_mask.py
+++ b/tests/integration/preprocessor/_mask/test_mask.py
@@ -182,13 +182,17 @@ class Test:
         np.ma.set_fill_value(expected, 1e+20)
         assert_array_equal(result_ice.data, expected)
 
-    def test_mask_fillvalues(self, mocker):
+    @pytest.mark.parametrize('lazy', [True, False])
+    def test_mask_fillvalues(self, mocker, lazy):
         """Test the fillvalues mask: func mask_fillvalues."""
         data_1 = data_2 = self.mock_data
         data_2.mask = np.ones((4, 3, 3), bool)
         coords_spec = [(self.times, 0), (self.lats, 1), (self.lons, 2)]
         cube_1 = iris.cube.Cube(data_1, dim_coords_and_dims=coords_spec)
         cube_2 = iris.cube.Cube(data_2, dim_coords_and_dims=coords_spec)
+        if lazy:
+            cube_1.data = cube_1.lazy_data().rechunk((2, None, None))
+            cube_2.data = cube_2.lazy_data()
         filename_1 = 'file1.nc'
         filename_2 = 'file2.nc'
         product_1 = mocker.create_autospec(
@@ -215,10 +219,17 @@ class Test:
                 result_1 = product.cubes[0]
             if product.filename == filename_2:
                 result_2 = product.cubes[0]
+
+        assert cube_1.has_lazy_data() == lazy
+        assert cube_2.has_lazy_data() == lazy
+        assert result_1.has_lazy_data() == lazy
+        assert result_2.has_lazy_data() == lazy
+
         assert_array_equal(result_2.data.mask, data_2.mask)
         assert_array_equal(result_1.data, data_1)
 
-    def test_mask_fillvalues_zero_threshold(self, mocker):
+    @pytest.mark.parametrize('lazy', [True, False])
+    def test_mask_fillvalues_zero_threshold(self, mocker, lazy):
         """Test the fillvalues mask: func mask_fillvalues for 0-threshold."""
         data_1 = self.mock_data
         data_2 = self.mock_data[0:3]
@@ -232,6 +243,10 @@ class Test:
         coords_spec2 = [(self.time2, 0), (self.lats, 1), (self.lons, 2)]
         cube_1 = iris.cube.Cube(data_1, dim_coords_and_dims=coords_spec)
         cube_2 = iris.cube.Cube(data_2, dim_coords_and_dims=coords_spec2)
+        if lazy:
+            cube_1.data = cube_1.lazy_data().rechunk((2, None, None))
+            cube_2.data = cube_2.lazy_data()
+
         filename_1 = Path('file1.nc')
         filename_2 = Path('file2.nc')
         product_1 = mocker.create_autospec(
@@ -255,6 +270,12 @@ class Test:
                 result_1 = product.cubes[0]
             if product.filename == filename_2:
                 result_2 = product.cubes[0]
+
+        assert cube_1.has_lazy_data() == lazy
+        assert cube_2.has_lazy_data() == lazy
+        assert result_1.has_lazy_data() == lazy
+        assert result_2.has_lazy_data() == lazy
+
         # identical masks
         assert_array_equal(
             result_2.data[0, ...].mask,
@@ -265,7 +286,8 @@ class Test:
         assert_array_equal(result_1[1:2].data.mask, cumulative_mask)
         assert_array_equal(result_2[2:3].data.mask, cumulative_mask)
 
-    def test_mask_fillvalues_min_value_none(self, mocker):
+    @pytest.mark.parametrize('lazy', [True, False])
+    def test_mask_fillvalues_min_value_none(self, mocker, lazy):
         """Test ``mask_fillvalues`` for min_value=None."""
         # We use non-masked data here and explicitly set some values to 0 here
         # since this caused problems in the past, see
@@ -278,6 +300,10 @@ class Test:
         coords_spec2 = [(self.time2, 0), (self.lats, 1), (self.lons, 2)]
         cube_1 = iris.cube.Cube(data_1, dim_coords_and_dims=coords_spec)
         cube_2 = iris.cube.Cube(data_2, dim_coords_and_dims=coords_spec2)
+        if lazy:
+            cube_1.data = cube_1.lazy_data().rechunk((2, None, None))
+            cube_2.data = cube_2.lazy_data()
+
         filename_1 = Path('file1.nc')
         filename_2 = Path('file2.nc')
 
@@ -303,10 +329,13 @@ class Test:
             min_value=None,
         )
 
+        assert cube_1.has_lazy_data() == lazy
+        assert cube_2.has_lazy_data() == lazy
         assert len(results) == 2
         for product in results:
             if product.filename in (filename_1, filename_2):
                 assert len(product.cubes) == 1
+                assert product.cubes[0].has_lazy_data() == lazy
                 assert not np.ma.is_masked(product.cubes[0].data)
             else:
                 assert False, f"Invalid filename: {product.filename}"
diff --git a/tests/integration/recipe/test_recipe.py b/tests/integration/recipe/test_recipe.py
index c50d837b6..d8133fc2b 100644
--- a/tests/integration/recipe/test_recipe.py
+++ b/tests/integration/recipe/test_recipe.py
@@ -2023,7 +2023,7 @@ def test_weighting_landsea_fraction_exclude(tmp_path, patched_datafinder,
                 additional_datasets:
                   - {dataset: CanESM2}
                   - {dataset: GFDL-CM3}
-                  - {dataset: TEST, project: obs4MIPs,
+                  - {dataset: TEST, project: obs4MIPs, tier: 1,
                      supplementary_variables: [{short_name: sftlf, mip: fx}]}
             scripts: null
         """)
@@ -3009,8 +3009,8 @@ def test_invalid_bias_type(tmp_path, patched_datafinder, session):
             scripts: null
         """)
     msg = (
-        "Expected one of ('absolute', 'relative') for `bias_type`, got "
-        "'INVALID'"
+        "Expected one of ('absolute', 'relative') for option `bias_type` of "
+        "preprocessor `bias`, got 'INVALID'"
     )
     with pytest.raises(RecipeError) as exc:
         get_recipe(tmp_path, content, session)
@@ -3283,8 +3283,43 @@ def test_invalid_metric(tmp_path, patched_datafinder, session):
         """)
     msg = (
         "Expected one of ('rmse', 'weighted_rmse', 'pearsonr', "
-        "'weighted_pearsonr', 'emd', 'weighted_emd') for `metric`, got "
-        "'INVALID'"
+        "'weighted_pearsonr', 'emd', 'weighted_emd') for option `metric` of "
+        "preprocessor `distance_metric`, got 'INVALID'"
+    )
+    with pytest.raises(RecipeError) as exc:
+        get_recipe(tmp_path, content, session)
+    assert str(exc.value) == INITIALIZATION_ERROR_MSG
+    assert exc.value.failed_tasks[0].message == msg
+
+
+def test_invalid_interpolate(tmp_path, patched_datafinder, session):
+    content = dedent("""
+        preprocessors:
+          test_resample_hours:
+            resample_hours:
+              interval: 1
+              interpolate: INVALID
+
+        diagnostics:
+          diagnostic_name:
+            variables:
+              ta:
+                preprocessor: test_resample_hours
+                project: CMIP6
+                mip: Amon
+                exp: historical
+                timerange: '20000101/20001231'
+                ensemble: r1i1p1f1
+                grid: gn
+                additional_datasets:
+                  - {dataset: CanESM5}
+                  - {dataset: CESM2, reference_for_bias: true}
+
+            scripts: null
+        """)
+    msg = (
+        "Expected one of (None, 'nearest', 'linear') for option `interpolate` "
+        "of preprocessor `resample_hours`, got 'INVALID'"
     )
     with pytest.raises(RecipeError) as exc:
         get_recipe(tmp_path, content, session)
diff --git a/tests/integration/test_local.py b/tests/integration/test_local.py
index 811431b33..74830130c 100644
--- a/tests/integration/test_local.py
+++ b/tests/integration/test_local.py
@@ -91,8 +91,8 @@ def test_find_files(monkeypatch, root, cfg):
     ref_globs = [
         Path(root, d, f) for d in cfg['dirs'] for f in cfg['file_patterns']
     ]
-    assert sorted([Path(f) for f in input_filelist]) == sorted(ref_files)
-    assert sorted([Path(g) for g in globs]) == sorted(ref_globs)
+    assert [Path(f) for f in input_filelist] == sorted(ref_files)
+    assert [Path(g) for g in globs] == sorted(ref_globs)
 
 
 def test_find_files_with_facets(monkeypatch, root):
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
index c84002779..ec88e281e 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -6,7 +6,7 @@ import yaml
 
 import esmvalcore
 from esmvalcore.cmor.check import CheckLevels
-from esmvalcore.config import CFG, _config
+from esmvalcore.config import CFG, _config, _config_validators
 from esmvalcore.config._config import (
     _deep_update,
     _load_extra_facets,
@@ -248,6 +248,9 @@ def test_project_obs4mips_case_correction(tmp_path, monkeypatch, mocker):
     assert 'obs4mips' not in _config.CFG
     assert _config.CFG['obs4MIPs'] == project_cfg
 
+    # Restore config-developer file
+    _config_validators.validate_config_developer(None)
+
 
 def test_load_config_developer_custom(tmp_path, monkeypatch, mocker):
     monkeypatch.setattr(_config, 'CFG', {})
@@ -261,6 +264,9 @@ def test_load_config_developer_custom(tmp_path, monkeypatch, mocker):
 
     assert 'custom' in _config.CFG
 
+    # Restore config-developer file
+    _config_validators.validate_config_developer(None)
+
 
 @pytest.mark.parametrize(
     'project,step',
diff --git a/tests/unit/config/test_config_validator.py b/tests/unit/config/test_config_validator.py
index b4d40b512..36dcd763f 100644
--- a/tests/unit/config/test_config_validator.py
+++ b/tests/unit/config/test_config_validator.py
@@ -21,6 +21,7 @@ from esmvalcore.config._config_validators import (
     validate_path,
     validate_path_or_none,
     validate_positive,
+    validate_rootpath,
     validate_search_esgf,
     validate_string,
     validate_string_or_none,
@@ -121,6 +122,7 @@ def generate_validator_testcases(valid):
                 ('a/b/c', Path.cwd() / 'a' / 'b' / 'c'),
                 ('/a/b/c/', Path('/', 'a', 'b', 'c')),
                 ('~/', Path.home()),
+                (Path.home(), Path.home()),
             ),
             'fail': (
                 (None, ValueError),
@@ -143,6 +145,49 @@ def generate_validator_testcases(valid):
                 ([], ValueError),
             ),
         },
+        {
+            'validator':
+            validate_rootpath,
+            'success': (
+                # Test a single path
+                ({
+                    'default': '/a'
+                }, {
+                    'default': [Path('/a')]
+                }),
+                ({
+                    'default': Path('/a')
+                }, {
+                    'default': [Path('/a')]
+                }),
+                # Test a list of paths
+                ({
+                    'CMIP6': ['/a', '/b']
+                }, {
+                    'CMIP6': [Path('/a'), Path('/b')]
+                }),
+                ({
+                    'CMIP6': [Path('/a'), Path('/b')]
+                }, {
+                    'CMIP6': [Path('/a'), Path('/b')]
+                }),
+                # Test a dict of paths
+                (
+                    {
+                        'CMIP6': {
+                            '/a': 'DKRZ',
+                            '/b': 'ESGF',
+                        },
+                    },
+                    {
+                        'CMIP6': {
+                            Path('/a'): 'DKRZ',
+                            Path('/b'): 'ESGF',
+                        },
+                    },
+                )),
+            'fail': (),
+        },
         {
             'validator': validate_positive,
             'success': (
@@ -287,3 +332,6 @@ def test_validate_config_developer(tmp_path):
 
     path = validate_config_developer(cfg_dev_file)
     assert path == cfg_dev_file
+
+    # Restore original config-developer file
+    validate_config_developer(None)
diff --git a/tests/unit/local/test_get_data_sources.py b/tests/unit/local/test_get_data_sources.py
new file mode 100644
index 000000000..3def03462
--- /dev/null
+++ b/tests/unit/local/test_get_data_sources.py
@@ -0,0 +1,65 @@
+from pathlib import Path
+
+import pytest
+
+from esmvalcore.config import CFG
+from esmvalcore.config._config_validators import validate_config_developer
+from esmvalcore.local import DataSource, _get_data_sources
+
+
+@pytest.mark.parametrize('rootpath_drs', [
+    (
+        {
+            'CMIP6': {
+                '/climate_data': 'ESGF'
+            }
+        },
+        {},
+    ),
+    (
+        {
+            'CMIP6': ['/climate_data']
+        },
+        {
+            'CMIP6': 'ESGF'
+        },
+    ),
+    (
+        {
+            'default': ['/climate_data']
+        },
+        {
+            'CMIP6': 'ESGF'
+        },
+    ),
+])
+def test_get_data_sources(monkeypatch, rootpath_drs):
+    # Make sure that default config-developer file is used
+    validate_config_developer(None)
+
+    rootpath, drs = rootpath_drs
+    monkeypatch.setitem(CFG, 'rootpath', rootpath)
+    monkeypatch.setitem(CFG, 'drs', drs)
+    sources = _get_data_sources('CMIP6')
+    source = sources[0]
+    assert isinstance(source, DataSource)
+    assert source.rootpath == Path('/climate_data')
+    assert '{project}' in source.dirname_template
+    assert '{short_name}' in source.filename_template
+
+
+def test_get_data_sources_nodefault(monkeypatch):
+    # Make sure that default config-developer file is used
+    validate_config_developer(None)
+
+    monkeypatch.setitem(
+        CFG,
+        'rootpath',
+        {
+            'CMIP5': {
+                '/climate_data': 'default'
+            },
+        },
+    )
+    with pytest.raises(KeyError):
+        _get_data_sources('CMIP6')
diff --git a/tests/unit/local/test_get_rootpath.py b/tests/unit/local/test_get_rootpath.py
deleted file mode 100644
index e300db199..000000000
--- a/tests/unit/local/test_get_rootpath.py
+++ /dev/null
@@ -1,28 +0,0 @@
-"""Tests for ``_get_rootpath`` in ``esmvalcore.local``."""
-from unittest import mock
-
-import pytest
-
-from esmvalcore import local
-
-
-@mock.patch("os.path.exists")
-def test_get_rootpath_exists(mexists):
-    mexists.return_value = True
-    cfg = {"rootpath": {"CMIP5": ["/path1"], "CMIP6": ["/path2"]}}
-    project = "CMIP5"
-    with mock.patch.dict(local.CFG, cfg):
-        output = local._get_rootpath(project)
-    # 'output' is a list containing a PosixPath:
-    assert str(output[0]) == cfg["rootpath"][project][0]
-
-
-@mock.patch("os.path.exists")
-def test_get_rootpath_does_not_exist(mexists):
-    mexists.return_value = False
-    cfg = {"rootpath": {"CMIP5": ["path1"], "CMIP6": ["path2"]}}
-    project = "OBS"
-    with mock.patch.dict(local.CFG, cfg):
-        msg = rf"The \"{project}\" option is missing.*"
-        with pytest.raises(KeyError, match=msg):
-            local._get_rootpath(project)
diff --git a/tests/unit/preprocessor/_mask/test_mask.py b/tests/unit/preprocessor/_mask/test_mask.py
index a3131c605..44cb0246f 100644
--- a/tests/unit/preprocessor/_mask/test_mask.py
+++ b/tests/unit/preprocessor/_mask/test_mask.py
@@ -5,6 +5,7 @@ import unittest
 import numpy as np
 
 import iris
+import iris.fileformats
 import tests
 from cf_units import Unit
 from esmvalcore.preprocessor._mask import (_apply_fx_mask,
diff --git a/tests/unit/preprocessor/_time/test_time.py b/tests/unit/preprocessor/_time/test_time.py
index a47da2a72..b478a9714 100644
--- a/tests/unit/preprocessor/_time/test_time.py
+++ b/tests/unit/preprocessor/_time/test_time.py
@@ -1899,7 +1899,7 @@ class TestResampleHours(tests.Test):
         return cube
 
     def test_resample_1_to_6(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 1)
         times = np.arange(0, 48, 1)
         cube = self._create_cube(data, times)
@@ -1909,7 +1909,7 @@ class TestResampleHours(tests.Test):
         assert_array_equal(result.data, expected)
 
     def test_resample_3_to_6(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 3)
         times = np.arange(0, 48, 3)
         cube = self._create_cube(data, times)
@@ -1919,7 +1919,7 @@ class TestResampleHours(tests.Test):
         assert_array_equal(result.data, expected)
 
     def test_resample_1_to_3(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 1)
         times = np.arange(0, 48, 1)
         cube = self._create_cube(data, times)
@@ -1929,7 +1929,7 @@ class TestResampleHours(tests.Test):
         assert_array_equal(result.data, expected)
 
     def test_resample_1_to_3_with_offset2(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 1)
         times = np.arange(0, 48, 1)
         cube = self._create_cube(data, times)
@@ -1939,7 +1939,7 @@ class TestResampleHours(tests.Test):
         assert_array_equal(result.data, expected)
 
     def test_resample_invalid(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 1)
         times = np.arange(0, 48, 1)
         cube = self._create_cube(data, times)
@@ -1948,7 +1948,7 @@ class TestResampleHours(tests.Test):
             resample_hours(cube, 5)
 
     def test_resample_invalid_offset(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 1)
         times = np.arange(0, 48, 1)
         cube = self._create_cube(data, times)
@@ -1957,7 +1957,7 @@ class TestResampleHours(tests.Test):
             resample_hours(cube, interval=3, offset=6)
 
     def test_resample_shorter_interval(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 12)
         times = np.arange(0, 48, 12)
         cube = self._create_cube(data, times)
@@ -1966,7 +1966,7 @@ class TestResampleHours(tests.Test):
             resample_hours(cube, interval=3)
 
     def test_resample_same_interval(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 48, 12)
         times = np.arange(0, 48, 12)
         cube = self._create_cube(data, times)
@@ -1976,7 +1976,7 @@ class TestResampleHours(tests.Test):
         assert_array_equal(result.data, expected)
 
     def test_resample_nodata(self):
-        """Test average of a 1D field."""
+        """Test ``resample_hours``."""
         data = np.arange(0, 4, 1)
         times = np.arange(0, 4, 1)
         cube = self._create_cube(data, times)
@@ -1984,6 +1984,37 @@ class TestResampleHours(tests.Test):
         with self.assertRaises(ValueError):
             resample_hours(cube, offset=5, interval=6)
 
+    def test_resample_interpolate_linear(self):
+        """Test ``resample_hours``."""
+        data = np.array([1, 2])
+        times = np.array([6, 18])
+        cube = self._create_cube(data, times)
+
+        result = resample_hours(cube, interval=12, interpolate='linear')
+        assert_array_equal(result.data, [0.5, 1.5])
+        assert_array_equal(result.coord('time').points, [0, 12])
+
+    def test_resample_interpolate_nearest(self):
+        """Test ``resample_hours``."""
+        data = np.array([1, 2])
+        times = np.array([6, 18])
+        cube = self._create_cube(data, times)
+
+        result = resample_hours(
+            cube, interval=12, offset=1, interpolate='nearest'
+        )
+        assert_array_equal(result.data, [1, 2])
+        assert_array_equal(result.coord('time').points, [1, 13])
+
+    def test_resample_invalid_interpolation(self):
+        """Test ``resample_hours``."""
+        data = np.arange(0, 4, 1)
+        times = np.arange(0, 4, 1)
+        cube = self._create_cube(data, times)
+
+        with self.assertRaises(ValueError):
+            resample_hours(cube, interval=1, interpolate='invalid')
+
 
 class TestResampleTime(tests.Test):
     """Test :func:`esmvalcore.preprocessor._time.resample_time`."""
diff --git a/tests/unit/preprocessor/_volume/test_volume.py b/tests/unit/preprocessor/_volume/test_volume.py
index e962e062d..f055d2f7b 100644
--- a/tests/unit/preprocessor/_volume/test_volume.py
+++ b/tests/unit/preprocessor/_volume/test_volume.py
@@ -2,6 +2,7 @@
 
 import unittest
 
+import dask.array as da
 import iris
 import iris.fileformats
 import numpy as np
@@ -118,6 +119,10 @@ class Test(tests.Test):
             units='kg m-3',
         )
 
+        self.grid_4d_lazy = self.grid_4d.copy()
+        self.grid_4d_lazy.data = self.grid_4d_lazy.lazy_data().rechunk(
+            (1, 2, None, None))
+
         coords_spec4_sigma = [(time, 0), (scoord, 1), (lats2, 2), (lons2, 3)]
         self.grid_4d_sigma_space = iris.cube.Cube(
             data2,
@@ -442,6 +447,18 @@ class Test(tests.Test):
         self.assertFalse(self.grid_4d.cell_measures('ocean_volume'))
         self.assertFalse(result.cell_measures('ocean_volume'))
 
+    def test_calculate_volume_lazy(self):
+        """Test that calculate_volume returns a lazy volume
+
+        The volume chunks should match those of the input cube for
+        computational efficiency.
+        """
+        chunks = self.grid_4d_lazy.core_data().chunks
+        volume = calculate_volume(self.grid_4d_lazy)
+        assert self.grid_4d_lazy.has_lazy_data()
+        assert isinstance(volume, da.Array)
+        assert volume.chunks == chunks
+
     def test_volume_statistics_cell_measure(self):
         """Test to take the volume weighted average of a (2,3,2,2) cube.
 
@@ -458,6 +475,23 @@ class Test(tests.Test):
         self.assert_array_equal(result.data, expected)
         self.assertEqual(result.units, 'kg m-3')
 
+    def test_volume_statistics_cell_measure_lazy(self):
+        """Test to take the volume weighted average of a lazy (2,3,2,2) cube.
+
+        The volume measure is pre-loaded in the cube.
+        """
+        grid_volume = calculate_volume(self.grid_4d_lazy)
+        measure = iris.coords.CellMeasure(grid_volume,
+                                          standard_name='ocean_volume',
+                                          units='m3',
+                                          measure='volume')
+        self.grid_4d_lazy.add_cell_measure(measure, range(0, measure.ndim))
+        result = volume_statistics(self.grid_4d_lazy, 'mean')
+        assert result.has_lazy_data()
+        expected = np.ma.array([1., 1.], mask=False)
+        self.assert_array_equal(result.data, expected)
+        self.assertEqual(result.units, 'kg m-3')
+
     def test_volume_statistics_long(self):
         """Test to take the volume weighted average of a (4,3,2,2) cube.
 
diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py
index 951f3f17f..25129dbf5 100644
--- a/tests/unit/test_dataset.py
+++ b/tests/unit/test_dataset.py
@@ -1189,28 +1189,26 @@ def test_from_recipe_with_glob(tmp_path, session, mocker):
           tas:
             project: CMIP5
             mip: Amon
+            exp: rcp85
+            ensemble: r1i1p1
             additional_datasets:
-              - {dataset: '*'}
+              - {dataset: '*', institute: '*'}
     """)
     recipe = tmp_path / 'recipe_test.yml'
     recipe.write_text(recipe_txt, encoding='utf-8')
 
     session['drs']['CMIP5'] = 'ESGF'
-
+    CFG['rootpath']['CMIP5'] = [tmp_path]
     filenames = [
-        "cmip5/output1/CSIRO-QCCCE/CSIRO-Mk3-6-0/rcp85/mon/atmos/Amon/r4i1p1/"
-        "v20120323/tas_Amon_CSIRO-Mk3-6-0_rcp85_r4i1p1_200601-210012.nc",
-        "cmip5/output1/NIMR-KMA/HadGEM2-AO/historical/mon/atmos/Amon/r1i1p1/"
-        "v20130815/tas_Amon_HadGEM2-AO_historical_r1i1p1_186001-200512.nc",
+        "cmip5/output1/CSIRO-QCCCE/CSIRO-Mk3-6-0/rcp85/mon/atmos/Amon/r1i1p1/"
+        "v20120323/tas_Amon_CSIRO-Mk3-6-0_rcp85_r1i1p1_200601-210012.nc",
+        "cmip5/output1/NIMR-KMA/HadGEM2-AO/rcp85/mon/atmos/Amon/r1i1p1/"
+        "v20130815/tas_Amon_HadGEM2-AO_rcp85_r1i1p1_186001-200512.nc",
     ]
-
-    mocker.patch.object(
-        esmvalcore.local,
-        '_get_input_filelist',
-        autospec=True,
-        spec_set=True,
-        return_value=(filenames, []),
-    )
+    for filename in filenames:
+        path = tmp_path / filename
+        path.parent.mkdir(parents=True, exist_ok=True)
+        path.write_text('')
 
     definitions = [
         {
@@ -1222,6 +1220,9 @@ def test_from_recipe_with_glob(tmp_path, session, mocker):
             'short_name': 'tas',
             'alias': 'CSIRO-Mk3-6-0',
             'recipe_dataset_index': 0,
+            'exp': 'rcp85',
+            'ensemble': 'r1i1p1',
+            'institute': 'CSIRO-QCCCE',
         },
         {
             'diagnostic': 'diagnostic1',
@@ -1232,6 +1233,9 @@ def test_from_recipe_with_glob(tmp_path, session, mocker):
             'short_name': 'tas',
             'alias': 'HadGEM2-AO',
             'recipe_dataset_index': 1,
+            'exp': 'rcp85',
+            'ensemble': 'r1i1p1',
+            'institute': 'NIMR-KMA',
         },
     ]
     expected = []