diff --git a/egcg_core/util.py b/egcg_core/util.py
index d1719af..0bfb634 100644
--- a/egcg_core/util.py
+++ b/egcg_core/util.py
@@ -100,3 +100,22 @@ def move_dir(src_dir, dest_dir):
             dest_file = os.path.join(dest_dir, os.path.basename(src_file))
             shutil.move(fp, dest_file)
     return 0
+
+
+def query_dict(data, query_string):
+    """
+    Drill down into a dict using dot notation, e.g. query_dict({'this': {'that': 'other'}}, 'this.that'}).
+    :param dict data:
+    :param str query_string:
+    """
+    _data = data.copy()
+
+    for q in query_string.split('.'):
+        d = _data.get(q)
+        if d is None:
+            return None
+
+        else:
+            _data = d
+
+    return _data

Dot-notated dict querying
We implement this quite a lot in multiple places, so can add this as a common function:

```python
>>> d = {'this': {'that': 'other'}}
>>> query_dict(d, 'this.that')
'other'
```
diff --git a/tests/test_util.py b/tests/test_util.py
index 7881fc3..fe703e8 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -74,7 +74,6 @@ class TestMoveDir(TestEGCG):
 
     def setUp(self):
         self.test_dir = join(self.assets_path, 'move_dir')
-        makedirs(join(self.test_dir, 'from'), exist_ok=True)
         makedirs(join(self.test_dir, 'from', 'subdir'), exist_ok=True)
         self._create_test_file(join(self.test_dir, 'from', 'ftest.txt'))
         self._create_test_file(join(self.test_dir, 'from', 'subdir', 'ftest.txt'))
@@ -83,7 +82,6 @@ class TestMoveDir(TestEGCG):
         self._create_test_file(join(self.test_dir, 'external', 'external.txt'), 'External file')
         symlink(join(self.test_dir, 'external', 'external.txt'), join(self.test_dir, 'from', 'external_renamed.txt'))
 
-        makedirs(join(self.test_dir, 'exists'), exist_ok=True)
         makedirs(join(self.test_dir, 'exists', 'subdir'), exist_ok=True)
         self._create_test_file(join(self.test_dir, 'exists', 'subdir', 'ftest.txt'), 'another file')
         self._create_test_file(join(self.test_dir, 'exists', 'ftest.txt'), 'another file')
@@ -127,3 +125,10 @@ class TestMoveDir(TestEGCG):
         assert util.find_file(to, 'ftest.txt')
         assert md5_from1 == self._md5(join(to, 'ftest.txt'))
         assert md5_from2 == self._md5(join(to, 'subdir', 'ftest.txt'))
+
+
+def test_query_dict():
+    data = {'this': {'that': 'other'}}
+    assert util.query_dict(data, 'this') == {'that': 'other'}
+    assert util.query_dict(data, 'this.that') == 'other'
+    assert util.query_dict(data, 'nonexistent') is None

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
asana==0.6.5
attrs==22.2.0
cached-property==1.5.2
certifi==2021.5.30
coverage==6.2
-e git+https://github.com/EdinburghGenomics/EGCG-Core.git@56a36841e0fedb399d9e5aab2c80c0468e93f2ed#egg=EGCG_Core
importlib-metadata==4.8.3
iniconfig==1.1.1
Jinja2==2.8
MarkupSafe==2.0.1
oauthlib==3.2.2
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyclarity-lims==0.4.8
pyparsing==3.1.4
pytest==7.0.1
pytest-cov==4.0.0
PyYAML==6.0.1
requests==2.14.2
requests-oauthlib==0.6.2
six==1.10.0
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0

name: EGCG-Core
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - asana==0.6.5
      - attrs==22.2.0
      - cached-property==1.5.2
      - coverage==6.2
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - jinja2==2.8
      - markupsafe==2.0.1
      - oauthlib==3.2.2
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pyclarity-lims==0.4.8
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - pyyaml==6.0.1
      - requests==2.14.2
      - requests-oauthlib==0.6.2
      - six==1.10.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - zipp==3.6.0
prefix: /opt/conda/envs/EGCG-Core

tests/test_util.py::test_query_dict
tests/test_util.py::test_find_files tests/test_util.py::test_find_file tests/test_util.py::test_str_join tests/test_util.py::test_find_fastqs tests/test_util.py::test_find_fastqs_with_lane tests/test_util.py::test_find_all_fastqs tests/test_util.py::test_find_all_fastq_pairs tests/test_util.py::test_same_fs tests/test_util.py::TestMoveDir::test_move_dir
egcg_core/util.py
egcg_core/util.py