Repository: aeye-lab/pymovements
Pull request #298 (2023-03-23 16:25:20)

Datasets: Use PublicDatasetDefinition to initialize a PublicDataset
## Description

This PR introduces the `PublicDatasetDefinition` class to initialize a `PublicDataset`.
The point with this is to clearly separate repsonsibilities:

The responsibility of the `PublicDataset` is now basically only the downloading and extracting of the dataset.
The information about the dataset itself has been completely moved to the new `PublicDatasetDefinition` class.

Each `PublicDatasetDefinition` is registered as a public dataset and can be referenced via string.
This makes it much more simple when using text configuration files, as you don't need to reference a classes anymore, but just use a string.

The `PublicDataset` initializer is now not crowded anymore which arguments. I expect that the number of attributes in defintions of public datasets will increase as we add new functionality like loading trial vars and a broad range of stimuli data.
Additionally, the `PublicDatasetDefinitions` are now made much more transparent as each attribute is public now.
All examples were adjusted for this.

Datasets can now no longer be initiliazed as so:
```python
dataset = pm.datasets.ToyDataset(root='data')
```

But are now initialized this way:
```python
dataset = pm.PublicDataset('ToyDataset', root='data')
```

You can also pass the class:
```python
dataset = pm.PublicDataset(pm.datasets.ToyDataset, root='data')
```

A `PublicDatasetDefinition` is a `dataclass` and can be registered using the  `@register_public_dataset` decorator:
```python
@dataclass
@register_public_dataset
class NewDataset(PublicDatasetDefinition):
    ...
```

As the definition is a `dataclass` you can also overwrite attributes via init:
```python
dataset = pm.PublicDataset(pm.datasets.ToyDataset(mirrors=['https://alternative.mirror.com']), root='data')
```

# Implemented changes

- [x] Move the datasets definitions to a separate directory
- [x] create a new decorator `@register_public_dataset` which registers datasets in a central dictionary
- [x] adjust `PublicDataset` class such that it now only accepts definitions.
- [x] adjusted examples and tutorials

## Type of change

- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change is or requires a documentation update

## How Has This Been Tested?

- [x] The public dataset tests where adjusted.
- [x] The correct registering of public datasets was tested.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings



Patch:
diff --git a/docs/source/tutorials/R-tutorial.md b/docs/source/tutorials/R-tutorial.md
index fa1ccc1..3cc04a5 100644
--- a/docs/source/tutorials/R-tutorial.md
+++ b/docs/source/tutorials/R-tutorial.md
@@ -27,7 +27,7 @@ Access functions and data within python modules and classes via the `$` operator
 To test, you can proceed with the "pymovements in 10 minutes" tutorial,
 for example this is how you download the ToyDataset:
 ```r
-dataset = pm$datasets$ToyDataset(root='data_directory')
+dataset = pm$PublicDataset('ToyDataset', root='data_directory')
 dataset$download()
 ```
 
diff --git a/docs/source/tutorials/detecting-events.ipynb b/docs/source/tutorials/detecting-events.ipynb
index 2d3aee4..44a21b6 100644
--- a/docs/source/tutorials/detecting-events.ipynb
+++ b/docs/source/tutorials/detecting-events.ipynb
@@ -62,7 +62,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()\n",
     "dataset.load()"
    ]
diff --git a/docs/source/tutorials/event-properties.ipynb b/docs/source/tutorials/event-properties.ipynb
index 81bfd42..9f30212 100644
--- a/docs/source/tutorials/event-properties.ipynb
+++ b/docs/source/tutorials/event-properties.ipynb
@@ -59,7 +59,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()\n",
     "dataset.load()"
    ]
diff --git a/docs/source/tutorials/heatmap.ipynb b/docs/source/tutorials/heatmap.ipynb
index 100d073..518900f 100644
--- a/docs/source/tutorials/heatmap.ipynb
+++ b/docs/source/tutorials/heatmap.ipynb
@@ -47,7 +47,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()\n",
     "dataset.load()"
    ]
diff --git a/docs/source/tutorials/local-dataset.ipynb b/docs/source/tutorials/local-dataset.ipynb
index 9761804..34de4ec 100644
--- a/docs/source/tutorials/local-dataset.ipynb
+++ b/docs/source/tutorials/local-dataset.ipynb
@@ -67,7 +67,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "toy_dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "toy_dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "toy_dataset.download(remove_finished=True)\n",
     "\n",
     "del toy_dataset"
diff --git a/docs/source/tutorials/plot-main-sequence.ipynb b/docs/source/tutorials/plot-main-sequence.ipynb
index 7c98a1b..d5c3aed 100644
--- a/docs/source/tutorials/plot-main-sequence.ipynb
+++ b/docs/source/tutorials/plot-main-sequence.ipynb
@@ -55,7 +55,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.toy_dataset.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()\n",
     "dataset.load()"
    ]
diff --git a/docs/source/tutorials/preprocessing-raw-data.ipynb b/docs/source/tutorials/preprocessing-raw-data.ipynb
index c18e422..f53b737 100644
--- a/docs/source/tutorials/preprocessing-raw-data.ipynb
+++ b/docs/source/tutorials/preprocessing-raw-data.ipynb
@@ -60,7 +60,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()\n",
     "dataset.load()"
    ]
diff --git a/docs/source/tutorials/public-datasets.ipynb b/docs/source/tutorials/public-datasets.ipynb
index 7c5466d..758da95 100644
--- a/docs/source/tutorials/public-datasets.ipynb
+++ b/docs/source/tutorials/public-datasets.ipynb
@@ -73,7 +73,8 @@
    "id": "c891cb45",
    "metadata": {},
    "source": [
-    "First we initialize the dataset by specifying the root data directory.\n",
+    "First we initialize our public dataset by specifying its name and the root data directory.\n",
+    "\n",
     "Our dataset will then be placed in a directory with the name of the dataset:"
    ]
   },
@@ -84,7 +85,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "\n",
     "dataset.path"
    ]
@@ -104,7 +105,13 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "pm.datasets.ToyDataset(root='data/', dataset_dirname='.').path"
+    "dataset = pm.PublicDataset(\n",
+    "    'ToyDataset',\n",
+    "    root='data/',\n",
+    "    dataset_dirname='.',\n",
+    ")\n",
+    "\n",
+    "dataset.path"
    ]
   },
   {
@@ -168,7 +175,13 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "pm.datasets.ToyDataset(root='data/', downloads_dirname='my_downloads').downloads_rootpath"
+    "dataset_2 = pm.PublicDataset(\n",
+    "    'ToyDataset',\n",
+    "    root='data/',\n",
+    "    downloads_dirname='new_downloads',\n",
+    ")\n",
+    "\n",
+    "dataset_2.downloads_rootpath"
    ]
   },
   {
diff --git a/docs/source/tutorials/pymovements-in-10-minutes.ipynb b/docs/source/tutorials/pymovements-in-10-minutes.ipynb
index 36120ba..861e070 100644
--- a/docs/source/tutorials/pymovements-in-10-minutes.ipynb
+++ b/docs/source/tutorials/pymovements-in-10-minutes.ipynb
@@ -76,7 +76,9 @@
     "\n",
     "Other datasets can be downloaded by simply replacing `ToyDataset` with one of the other available datasets.\n",
     "\n",
-    "We can initialize and download the dataset using these simple lines of code:"
+    "We can initialize and download by passing the desired dataset name as a string argument.\n",
+    "\n",
+    "Additionally we need the root directory path of your data."
    ]
   },
   {
@@ -86,10 +88,28 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "a9fe1e46",
+   "metadata": {},
+   "source": [
+    "Our downloaded dataset will be placed in new a directory with the name of the dataset:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "fa402978",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset.path"
+   ]
+  },
   {
    "cell_type": "markdown",
    "id": "b82bcfe3",
@@ -432,7 +452,7 @@
    "id": "7836c285",
    "metadata": {},
    "source": [
-    "Let's test this out by initializing a new `Dataset` object in the same directory and loading in the preprocessed gaze and event data.\n",
+    "Let's test this out by initializing a new `PublicDataset` object in the same directory and loading in the preprocessed gaze and event data.\n",
     "\n",
     "This time we don't need to download anything."
    ]
@@ -444,7 +464,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "preprocessed_dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "preprocessed_dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "\n",
     "dataset.load(events=True, preprocessed=True, subset=subset)\n",
     "\n",
diff --git a/docs/source/tutorials/saving-loading-events.ipynb b/docs/source/tutorials/saving-loading-events.ipynb
index 5f94339..9597b72 100644
--- a/docs/source/tutorials/saving-loading-events.ipynb
+++ b/docs/source/tutorials/saving-loading-events.ipynb
@@ -60,7 +60,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()\n",
     "dataset.load()"
    ]
@@ -242,7 +242,7 @@
    "source": [
     "Now let's imagine that this preprocessing and saving was done in another file and we only want to load the preprocessed data.\n",
     "\n",
-    "We simulate this by initializing a new dataset. We don't need to download any additional data."
+    "We simulate this by initializing a new dataset object. We don't need to download any additional data."
    ]
   },
   {
@@ -252,7 +252,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "preprocessed_dataset = pm.datasets.ToyDataset(root='data/')"
+    "preprocessed_dataset = pm.PublicDataset('ToyDataset', root='data/')"
    ]
   },
   {
diff --git a/docs/source/tutorials/saving-loading-preprocessed.ipynb b/docs/source/tutorials/saving-loading-preprocessed.ipynb
index bd6919a..0a7d7f3 100644
--- a/docs/source/tutorials/saving-loading-preprocessed.ipynb
+++ b/docs/source/tutorials/saving-loading-preprocessed.ipynb
@@ -60,7 +60,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "dataset = pm.datasets.ToyDataset(root='data/')\n",
+    "dataset = pm.PublicDataset('ToyDataset', root='data/')\n",
     "dataset.download()\n",
     "dataset.load()"
    ]
@@ -210,7 +210,7 @@
    "source": [
     "Now let's imagine that this preprocessing and saving was done in another file and we only want to load the preprocessed data.\n",
     "\n",
-    "We simulate this by initializing a new dataset. We don't need to download any additional data."
+    "We simulate this by initializing a new dataset object. We don't need to download any additional data."
    ]
   },
   {
@@ -220,7 +220,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "preprocessed_dataset = pm.datasets.ToyDataset(root='data/')"
+    "preprocessed_dataset = pm.PublicDataset('ToyDataset', root='data/')"
    ]
   },
   {
diff --git a/src/pymovements/__init__.py b/src/pymovements/__init__.py
index 7bfc5f7..fc32c91 100644
--- a/src/pymovements/__init__.py
+++ b/src/pymovements/__init__.py
@@ -24,3 +24,6 @@ from pymovements import gaze  # noqa: F401
 from pymovements import plotting  # noqa: F401
 from pymovements import synthetic  # noqa: F401
 from pymovements import utils  # noqa: F401
+from pymovements.datasets import Dataset  # noqa: F401
+from pymovements.datasets import PublicDataset  # noqa: F401
+from pymovements.datasets import PublicDatasetDefinition  # noqa: F401
diff --git a/src/pymovements/datasets/__init__.py b/src/pymovements/datasets/__init__.py
index 33d25da..bb6d747 100644
--- a/src/pymovements/datasets/__init__.py
+++ b/src/pymovements/datasets/__init__.py
@@ -27,19 +27,22 @@
 
     pymovements.datasets.Dataset
     pymovements.datasets.PublicDataset
+    pymovements.datasets.PublicDatasetDefinition
 
-.. rubric:: Public Datasets
+.. rubric:: Public Dataset Definitions
 
 .. autosummary::
    :toctree:
    :template: class.rst
 
-    pymovements.datasets.ToyDataset
-    pymovements.datasets.GazeBase
-    pymovements.datasets.JuDo1000
+    pymovements.datasets.definitions.ToyDataset
+    pymovements.datasets.definitions.GazeBase
+    pymovements.datasets.definitions.JuDo1000
 """
+from pymovements.datasets import definitions  # noqa:F401
 from pymovements.datasets.dataset import Dataset  # noqa: F401
-from pymovements.datasets.gazebase import GazeBase  # noqa: F401
-from pymovements.datasets.judo1000 import JuDo1000  # noqa: F401
+from pymovements.datasets.definitions import GazeBase  # noqa: F401
+from pymovements.datasets.definitions import JuDo1000  # noqa: F401
+from pymovements.datasets.definitions import ToyDataset  # noqa: F401
 from pymovements.datasets.public_dataset import PublicDataset  # noqa: F401
-from pymovements.datasets.toy_dataset import ToyDataset  # noqa: F401
+from pymovements.datasets.public_dataset import PublicDatasetDefinition  # noqa: F401
diff --git a/src/pymovements/datasets/definitions/__init__.py b/src/pymovements/datasets/definitions/__init__.py
new file mode 100644
index 0000000..dd9b07f
--- /dev/null
+++ b/src/pymovements/datasets/definitions/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""This module holds all dataset definitions."""
+from pymovements.datasets.definitions.gazebase import GazeBase  # noqa: F401
+from pymovements.datasets.definitions.judo1000 import JuDo1000  # noqa: F401
+from pymovements.datasets.definitions.toy_dataset import ToyDataset  # noqa: F401
diff --git a/src/pymovements/datasets/definitions/gazebase.py b/src/pymovements/datasets/definitions/gazebase.py
new file mode 100644
index 0000000..dbad419
--- /dev/null
+++ b/src/pymovements/datasets/definitions/gazebase.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2022-2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""This module provides an interface to the GazeBase dataset."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from dataclasses import field
+from typing import Any
+
+from pymovements.datasets.public_dataset import PublicDatasetDefinition
+from pymovements.datasets.public_dataset import register_public_dataset
+from pymovements.gaze.experiment import Experiment
+
+
+@dataclass
+@register_public_dataset
+class GazeBase(PublicDatasetDefinition):
+    """GazeBase dataset :cite:p:`GazeBase`.
+
+    This dataset includes monocular (left eye) eye tracking data from 322 participants captured over
+    a period of 37 months. Participants attended up to 9 rounds during this time frame, with each
+    round consisting of two contiguous sessions.
+
+    Eye movements are recorded at a sampling frequency of 1000 Hz using an EyeLink 1000 video-based
+    eye tracker and are provided as positional data in degrees of visual angle.
+
+    In each of the two sessions per round, participants are instructed to complete a series of
+    tasks, including a fixation task (FIX), a horizontal saccade task (HSS), a random saccade task
+    (RAN), a reading task (TEX), two free viewing video tasks (VD1 and VD2) and a gaze-driven gaming
+    task (BLG).
+
+    Check the respective paper for details :cite:p:`GazeBase`.
+
+    Attributes
+    ----------
+    name : str
+        The name of the dataset.
+
+    mirrors : tuple[str, ...]
+        A tuple of mirrors of the dataset. Each entry must be of type `str` and end with a '/'.
+
+    resources : tuple[dict[str, str], ...]
+        A tuple of dataset resources. Each list entry must be a dictionary with the following keys:
+        - `resource`: The url suffix of the resource. This will be concatenated with the mirror.
+        - `filename`: The filename under which the file is saved as.
+        - `md5`: The MD5 checksum of the respective file.
+
+    experiment : Experiment
+        The experiment definition.
+
+    filename_regex : str
+        Regular expression which will be matched before trying to load the file. Namedgroups will
+        appear in the `fileinfo` dataframe.
+
+    filename_regex_dtypes : dict[str, type], optional
+        If named groups are present in the `filename_regex`, this makes it possible to cast specific
+        named groups to a particular datatype.
+
+    column_map : dict[str, str]
+        The keys are the columns to read, the values are the names to which they should be renamed.
+
+    custom_read_kwargs : dict[str, Any], optional
+        If specified, these keyword arguments will be passed to the file reading function.
+
+    Examples
+    --------
+    Initialize your :py:class:`~pymovements.PublicDataset` object with the
+    :py:class:`~pymovements.GazeBase` definition:
+
+    >>> import pymovements as pm
+    >>>
+    >>> dataset = pm.PublicDataset("GazeBase", root='data/')
+
+    Download the dataset resources resources:
+
+    >>> dataset.download()# doctest: +SKIP
+
+    Load the data into memory:
+
+    >>> dataset.load()# doctest: +SKIP
+    """
+    # pylint: disable=similarities
+    # The PublicDatasetDefinition child classes potentially share code chunks for definitions.
+
+    name: str = 'GazeBase'
+
+    mirrors: tuple[str] = (
+        'https://figshare.com/ndownloader/files/',
+    )
+
+    resources: tuple[dict[str, str]] = (
+        {
+            'resource': '27039812',
+            'filename': 'GazeBase_v2_0.zip',
+            'md5': 'cb7eb895fb48f8661decf038ab998c9a',
+        },
+    )
+
+    experiment: Experiment = Experiment(
+        screen_width_px=1680,
+        screen_height_px=1050,
+        screen_width_cm=47.4,
+        screen_height_cm=29.7,
+        distance_cm=55,
+        origin='lower left',
+        sampling_rate=1000,
+    )
+
+    filename_regex: str = (
+        r'S_(?P<round_id>\d)(?P<subject_id>\d+)'
+        r'_S(?P<session_id>\d+)'
+        r'_(?P<task_name>.+).csv'
+    )
+
+    filename_regex_dtypes: dict[str, type] = field(
+        default_factory=lambda: {
+            'round_id': int,
+            'subject_id': int,
+            'session_id': int,
+        },
+    )
+
+    column_map: dict[str, str] = field(
+        default_factory=lambda: {
+            'n': 'time',
+            'x': 'x_left_pos',
+            'y': 'y_left_pos',
+            'val': 'validity',
+            'xT': 'x_target_pos',
+            'yT': 'y_target_pos',
+        },
+    )
+
+    custom_read_kwargs: dict[str, Any] = field(default_factory=dict)
diff --git a/src/pymovements/datasets/definitions/judo1000.py b/src/pymovements/datasets/definitions/judo1000.py
new file mode 100644
index 0000000..094080a
--- /dev/null
+++ b/src/pymovements/datasets/definitions/judo1000.py
@@ -0,0 +1,144 @@
+# Copyright (c) 2022-2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""This module provides an interface to the JuDo1000 dataset."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from dataclasses import field
+
+from pymovements.datasets.public_dataset import PublicDatasetDefinition
+from pymovements.datasets.public_dataset import register_public_dataset
+from pymovements.gaze.experiment import Experiment
+
+
+@dataclass
+@register_public_dataset
+class JuDo1000(PublicDatasetDefinition):
+    """JuDo1000 dataset :cite:p:`JuDo1000`.
+
+    This dataset includes binocular eye tracking data from 150 participants in four sessions with an
+    interval of at least one week between two sessions. Eye movements are recorded at a sampling
+    frequency of 1000 Hz using an EyeLink Portable Duo video-based eye tracker and are provided as
+    pixel coordinates. Participants are instructed to watch a random jumping dot on a computer
+    screen.
+
+    Check the respective `repository <https://osf.io/5zpvk/>`_ for details.
+
+    Attributes
+    ----------
+    name : str
+        The name of the dataset.
+
+    mirrors : tuple[str, ...]
+        A tuple of mirrors of the dataset. Each entry must be of type `str` and end with a '/'.
+
+    resources : tuple[dict[str, str], ...]
+        A tuple of dataset resources. Each list entry must be a dictionary with the following keys:
+        - `resource`: The url suffix of the resource. This will be concatenated with the mirror.
+        - `filename`: The filename under which the file is saved as.
+        - `md5`: The MD5 checksum of the respective file.
+
+    experiment : Experiment
+        The experiment definition.
+
+    filename_regex : str
+        Regular expression which will be matched before trying to load the file. Namedgroups will
+        appear in the `fileinfo` dataframe.
+
+    filename_regex_dtypes : dict[str, type], optional
+        If named groups are present in the `filename_regex`, this makes it possible to cast specific
+        named groups to a particular datatype.
+
+    column_map : dict[str, str]
+        The keys are the columns to read, the values are the names to which they should be renamed.
+
+    custom_read_kwargs : dict[str, Any], optional
+        If specified, these keyword arguments will be passed to the file reading function.
+
+    Examples
+    --------
+    Initialize your :py:class:`~pymovements.PublicDataset` object with the
+    :py:class:`~pymovements.JuDo1000` definition:
+
+    >>> import pymovements as pm
+    >>>
+    >>> dataset = pm.PublicDataset("JuDo1000", root='data/')
+
+    Download the dataset resources resources:
+
+    >>> dataset.download()# doctest: +SKIP
+
+    Load the data into memory:
+
+    >>> dataset.load()# doctest: +SKIP
+    """
+    # pylint: disable=similarities
+    # The PublicDatasetDefinition child classes potentially share code chunks for definitions.
+
+    name: str = 'JuDo1000'
+
+    mirrors: tuple[str, ...] = (
+        'https://osf.io/download/',
+    )
+
+    resources: tuple[dict[str, str], ...] = (
+        {
+            'resource': '4wy7s/',
+            'filename': 'JuDo1000.zip',
+            'md5': 'b8b9e5bb65b78d6f2bd260451cdd89f8',
+        },
+    )
+
+    experiment: Experiment = Experiment(
+        screen_width_px=1280,
+        screen_height_px=1024,
+        screen_width_cm=38,
+        screen_height_cm=30.2,
+        distance_cm=68,
+        origin='lower left',
+        sampling_rate=1000,
+    )
+
+    filename_regex: str = r'(?P<subject_id>\d+)_(?P<session_id>\d+).csv'
+
+    filename_regex_dtypes: dict[str, type] = field(
+        default_factory=lambda: {
+            'subject_id': int,
+            'session_id': int,
+        },
+    )
+
+    column_map: dict[str, str] = field(
+        default_factory=lambda: {
+            'trialId': 'trial_id',
+            'pointId': 'point_id',
+            'time': 'time',
+            'x_left': 'x_left_pix',
+            'y_left': 'y_left_pix',
+            'x_right': 'x_right_pix',
+            'y_right': 'y_right_pix',
+        },
+    )
+
+    custom_read_kwargs: dict[str, str] = field(
+        default_factory=lambda: {
+            'separator': '\t',
+        },
+    )
diff --git a/src/pymovements/datasets/definitions/toy_dataset.py b/src/pymovements/datasets/definitions/toy_dataset.py
new file mode 100644
index 0000000..67d1258
--- /dev/null
+++ b/src/pymovements/datasets/definitions/toy_dataset.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""This module provides an interface to the pymovements example toy dataset."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from dataclasses import field
+
+from pymovements.datasets.public_dataset import PublicDatasetDefinition
+from pymovements.datasets.public_dataset import register_public_dataset
+from pymovements.gaze.experiment import Experiment
+
+
+@dataclass
+@register_public_dataset
+class ToyDataset(PublicDatasetDefinition):
+    """Example toy dataset.
+
+    This dataset includes monocular eye tracking data from a single participants in a single
+    session. Eye movements are recorded at a sampling frequency of 1000 Hz using an EyeLink Portable
+    Duo video-based eye tracker and are provided as pixel coordinates.
+
+    The participant is instructed to read 4 texts with 5 screens each.
+
+    Attributes
+    ----------
+    name : str
+        The name of the dataset.
+
+    mirrors : tuple[str, ...]
+        A tuple of mirrors of the dataset. Each entry must be of type `str` and end with a '/'.
+
+    resources : tuple[dict[str, str], ...]
+        A tuple of dataset resources. Each list entry must be a dictionary with the following keys:
+        - `resource`: The url suffix of the resource. This will be concatenated with the mirror.
+        - `filename`: The filename under which the file is saved as.
+        - `md5`: The MD5 checksum of the respective file.
+
+    experiment : Experiment
+        The experiment definition.
+
+    filename_regex : str
+        Regular expression which will be matched before trying to load the file. Namedgroups will
+        appear in the `fileinfo` dataframe.
+
+    filename_regex_dtypes : dict[str, type], optional
+        If named groups are present in the `filename_regex`, this makes it possible to cast specific
+        named groups to a particular datatype.
+
+    column_map : dict[str, str]
+        The keys are the columns to read, the values are the names to which they should be renamed.
+
+    custom_read_kwargs : dict[str, Any], optional
+        If specified, these keyword arguments will be passed to the file reading function.
+
+    Examples
+    --------
+    Initialize your :py:class:`~pymovements.PublicDataset` object with the
+    :py:class:`~pymovements.ToyDataset` definition:
+
+    >>> import pymovements as pm
+    >>>
+    >>> dataset = pm.PublicDataset("ToyDataset", root='data/')
+
+    Download the dataset resources resources:
+
+    >>> dataset.download()# doctest: +SKIP
+
+    Load the data into memory:
+
+    >>> dataset.load()# doctest: +SKIP
+    """
+    # pylint: disable=similarities
+    # The PublicDatasetDefinition child classes potentially share code chunks for definitions.
+
+    name: str = 'ToyDataset'
+
+    mirrors: tuple[str, ...] = (
+        'http://github.com/aeye-lab/pymovements-toy-dataset/zipball/',
+    )
+
+    resources: tuple[dict[str, str], ...] = (
+        {
+            'resource': '6cb5d663317bf418cec0c9abe1dde5085a8a8ebd/',
+            'filename': 'pymovements-toy-dataset.zip',
+            'md5': '4da622457637a8181d86601fe17f3aa8',
+        },
+    )
+
+    experiment: Experiment = Experiment(
+        screen_width_px=1280,
+        screen_height_px=1024,
+        screen_width_cm=38,
+        screen_height_cm=30.2,
+        distance_cm=68,
+        origin='lower left',
+        sampling_rate=1000,
+    )
+
+    filename_regex: str = r'trial_(?P<text_id>\d+)_(?P<page_id>\d+).csv'
+
+    filename_regex_dtypes: dict[str, type] = field(
+        default_factory=lambda: {
+            'text_id': int,
+            'page_id': int,
+        },
+    )
+
+    column_map: dict[str, str] = field(
+        default_factory=lambda: {
+            'timestamp': 'time',
+            'x': 'x_right_pix',
+            'y': 'y_right_pix',
+        },
+    )
+
+    custom_read_kwargs: dict[str, str] = field(
+        default_factory=lambda: {
+            'separator': '\t',
+            'null_values': '-32768.00',
+        },
+    )
diff --git a/src/pymovements/datasets/gazebase.py b/src/pymovements/datasets/gazebase.py
deleted file mode 100644
index 131f4bf..0000000
--- a/src/pymovements/datasets/gazebase.py
+++ /dev/null
@@ -1,193 +0,0 @@
-# Copyright (c) 2022-2023 The pymovements Project Authors
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-"""This module provides an interface to the GazeBase dataset."""
-from __future__ import annotations
-
-from pathlib import Path
-
-from pymovements.datasets.public_dataset import PublicDataset
-from pymovements.gaze.experiment import Experiment
-
-
-class GazeBase(PublicDataset):
-    """GazeBase dataset :cite:p:`GazeBase`.
-
-    This dataset includes monocular (left eye) eye tracking data from 322 participants captured over
-    a period of 37 months. Participants attended up to 9 rounds during this time frame, with each
-    round consisting of two contiguous sessions.
-
-    Eye movements are recorded at a sampling frequency of 1000 Hz using an EyeLink 1000 video-based
-    eye tracker and are provided as positional data in degrees of visual angle.
-
-    In each of the two sessions per round, participants are instructed to complete a series of
-    tasks, including a fixation task (FIX), a horizontal saccade task (HSS), a random saccade task
-    (RAN), a reading task (TEX), two free viewing video tasks (VD1 and VD2) and a gaze-driven gaming
-    task (BLG).
-
-    Check the respective paper for details :cite:p:`GazeBase`.
-
-    Examples
-    --------
-
-    This will initialize your :py:class:`pymovements.PublicDataset` object, download its resources
-    and load the data into memory.
-
-    >>> dataset = GazeBase(root='data/')
-    >>> dataset.download()  # doctest: +SKIP
-    >>> dataset.load()  # doctest: +SKIP
-    """
-    # pylint: disable=similarities
-    # The PublicDataset child classes potentially share code chunks for definitions.
-
-    _mirrors = [
-        'https://figshare.com/ndownloader/files/',
-    ]
-
-    _resources = [
-        {
-            'resource': '27039812',
-            'filename': 'GazeBase_v2_0.zip',
-            'md5': 'cb7eb895fb48f8661decf038ab998c9a',
-        },
-    ]
-
-    _experiment = Experiment(
-        screen_width_px=1680,
-        screen_height_px=1050,
-        screen_width_cm=47.4,
-        screen_height_cm=29.7,
-        distance_cm=55,
-        origin='lower left',
-        sampling_rate=1000,
-    )
-
-    _filename_regex = (
-        r'S_(?P<round_id>\d)(?P<subject_id>\d+)'
-        r'_S(?P<session_id>\d+)'
-        r'_(?P<task_name>.+).csv'
-    )
-
-    _filename_regex_dtypes = {
-        'round_id': int,
-        'subject_id': int,
-        'session_id': int,
-    }
-
-    _column_map = {
-        'n': 'time',
-        'x': 'x_left_pos',
-        'y': 'y_left_pos',
-        'val': 'validity',
-        'xT': 'x_target_pos',
-        'yT': 'y_target_pos',
-    }
-
-    _read_csv_kwargs = {
-        'columns': list(_column_map.keys()),
-        'new_columns': list(_column_map.values()),
-    }
-
-    def __init__(
-            self,
-            root: str | Path,
-            dataset_dirname: str = 'GazeBase',
-            downloads_dirname: str = 'downloads',
-            raw_dirname: str = 'raw',
-            preprocessed_dirname: str = 'preprocessed',
-            events_dirname: str = 'events',
-    ):
-        """Initialize the GazeBase dataset object.
-
-        If desired, dataset resources are downloaded with ``download=True`` and extracted with
-        ``extract=True``. To save space on your device you can remove the archive files after
-        successful extraction with ``remove_finished=True``.
-
-        Downloaded archives are automatically checked for integrity by comparing MD5 checksums.
-
-        You can set up a custom directory structure by populating the particular dirname attributes.
-        See :py:attr:`~pymovements.dataset.PublicDataset.dataset_dirname`,
-        :py:attr:`~pymovements.dataset.PublicDataset.raw_dirname`,
-        :py:attr:`~pymovements.dataset.PublicDataset.preprocessed_dirname` and
-        :py:attr:`~pymovements.dataset.PublicDataset.events_dirname` and
-        :py:attr:`~pymovements.dataset.PublicDataset.downloads_dirname` for details.
-
-        Parameters
-        ----------
-        root : str, Path
-            Path to the root directory of the dataset.
-        dataset_dirname : str, optional
-            Dataset directory name under root path. Can be `.` if dataset is located in root path.
-            Default: `.`
-        downloads_dirname : str, optional
-            Name of directory to store downloaded data.Default: `downloads`
-        raw_dirname ; str, optional
-            Name of directory under dataset path that contains raw data. Can be `.` if raw data is
-            located in dataset path. We advise the user to keep the original raw data separate from
-            the preprocessed / event data. Default: `raw`
-        preprocessed_dirname : str, optional
-            Name of directory under dataset path that will be used to store preprocessed data. We
-            advise the user to keep the preprocessed data separate from the original raw data.
-            Default: `preprocessed`
-        events_dirname : str, optional
-            Name of directory under dataset path that will be used to store event data. We advise
-            the user to keep the event data separate from the original raw data. Default: `events`
-        """
-        super().__init__(
-            root=root,
-            experiment=self._experiment,
-            filename_regex=self._filename_regex,
-            filename_regex_dtypes=self._filename_regex_dtypes,
-            custom_read_kwargs=self._read_csv_kwargs,
-            dataset_dirname=dataset_dirname,
-            downloads_dirname=downloads_dirname,
-            raw_dirname=raw_dirname,
-            preprocessed_dirname=preprocessed_dirname,
-            events_dirname=events_dirname,
-        )
-
-    @property
-    def path(self) -> Path:
-        """Get the path to the dataset directory.
-
-        The dataset path points to the dataset directory under the root path. Per default the
-        dataset directory name is equal to the class name.
-
-        Example
-        -------
-        The default behaviour is to locate the data set in a directory under the root path with the
-        same name as the class name:
-        >>> dataset = GazeBase(root='/path/to/all/your/datasets')
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/all/your/datasets/GazeBase')
-
-        You can specify an explicit dataset directory name:
-        >>> dataset = GazeBase(
-        ...     root='/path/to/all/your/datasets',
-        ...     dataset_dirname='gazebase',
-        ... )
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/all/your/datasets/gazebase')
-
-        You can specify to use the root path to be the actual dataset directory:
-        >>> dataset = GazeBase(root='/path/to/gazebase/dataset', dataset_dirname='.')
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/gazebase/dataset')
-        """
-        return super().path
diff --git a/src/pymovements/datasets/judo1000.py b/src/pymovements/datasets/judo1000.py
deleted file mode 100644
index 767b8e6..0000000
--- a/src/pymovements/datasets/judo1000.py
+++ /dev/null
@@ -1,183 +0,0 @@
-# Copyright (c) 2022-2023 The pymovements Project Authors
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-"""This module provides an interface to the JuDo1000 dataset."""
-from __future__ import annotations
-
-from pathlib import Path
-
-from pymovements.datasets.public_dataset import PublicDataset
-from pymovements.gaze.experiment import Experiment
-
-
-class JuDo1000(PublicDataset):
-    """JuDo1000 dataset :cite:p:`JuDo1000`.
-
-    This dataset includes binocular eye tracking data from 150 participants in four sessions with an
-    interval of at least one week between two sessions. Eye movements are recorded at a sampling
-    frequency of 1000 Hz using an EyeLink Portable Duo video-based eye tracker and are provided as
-    pixel coordinates. Participants are instructed to watch a random jumping dot on a computer
-    screen.
-
-    Check the respective `repository <https://osf.io/5zpvk/>`_ for details.
-
-    Examples
-    --------
-    This will initialize your :py:class:`pymovements.PublicDataset` object, download its resources
-    and load the data into memory.
-
-    >>> dataset = JuDo1000(root='data/')
-    >>> dataset.download()  # doctest: +SKIP
-    >>> dataset.load()  # doctest: +SKIP
-    """
-    # pylint: disable=similarities
-    # The PublicDataset child classes potentially share code chunks for definitions.
-
-    _mirrors = [
-        'https://osf.io/download/',
-    ]
-
-    _resources = [
-        {
-            'resource': '4wy7s/',
-            'filename': 'JuDo1000.zip',
-            'md5': 'b8b9e5bb65b78d6f2bd260451cdd89f8',
-        },
-    ]
-
-    _experiment = Experiment(
-        screen_width_px=1280,
-        screen_height_px=1024,
-        screen_width_cm=38,
-        screen_height_cm=30.2,
-        distance_cm=68,
-        origin='lower left',
-        sampling_rate=1000,
-    )
-
-    _filename_regex = r'(?P<subject_id>\d+)_(?P<session_id>\d+).csv'
-
-    _filename_regex_dtypes = {
-        'subject_id': int,
-        'session_id': int,
-    }
-
-    _column_map = {
-        'trialId': 'trial_id',
-        'pointId': 'point_id',
-        'time': 'time',
-        'x_left': 'x_left_pix',
-        'y_left': 'y_left_pix',
-        'x_right': 'x_right_pix',
-        'y_right': 'y_right_pix',
-    }
-
-    _read_csv_kwargs = {
-        'sep': '\t',
-        'columns': list(_column_map.keys()),
-        'new_columns': list(_column_map.values()),
-    }
-
-    def __init__(
-            self,
-            root: str | Path,
-            dataset_dirname: str = 'JuDo1000',
-            downloads_dirname: str = 'downloads',
-            raw_dirname: str = 'raw',
-            preprocessed_dirname: str = 'preprocessed',
-            events_dirname: str = 'events',
-    ):
-        """Initialize the JuDo1000 dataset object.
-
-        If desired, dataset resources are downloaded with ``download=True`` and extracted with
-        ``extract=True``. To save space on your device you can remove the archive files after
-        successful extraction with ``remove_finished=True``.
-
-        Downloaded archives are automatically checked for integrity by comparing MD5 checksums.
-
-        You can set up a custom directory structure by populating the particular dirname attributes.
-        See :py:attr:`~pymovements.dataset.PublicDataset.dataset_dirname`,
-        :py:attr:`~pymovements.dataset.PublicDataset.raw_dirname`,
-        :py:attr:`~pymovements.dataset.PublicDataset.preprocessed_dirname` and
-        :py:attr:`~pymovements.dataset.PublicDataset.events_dirname` and
-        :py:attr:`~pymovements.dataset.PublicDataset.downloads_dirname` for details.
-
-        Parameters
-        ----------
-        root : str, Path
-            Path to the root directory of the dataset.
-        dataset_dirname : str, optional
-            Dataset directory name under root path. Can be `.` if dataset is located in root path.
-            Default: `.`
-        downloads_dirname : str, optional
-            Name of directory to store downloaded data.Default: `downloads`
-        raw_dirname ; str, optional
-            Name of directory under dataset path that contains raw data. Can be `.` if raw data is
-            located in dataset path. We advise the user to keep the original raw data separate from
-            the preprocessed / event data. Default: `raw`
-        preprocessed_dirname : str, optional
-            Name of directory under dataset path that will be used to store preprocessed data. We
-            advise the user to keep the preprocessed data separate from the original raw data.
-            Default: `preprocessed`
-        events_dirname : str, optional
-            Name of directory under dataset path that will be used to store event data. We advise
-            the user to keep the event data separate from the original raw data. Default: `events`
-        """
-        super().__init__(
-            root=root,
-            experiment=self._experiment,
-            filename_regex=self._filename_regex,
-            filename_regex_dtypes=self._filename_regex_dtypes,
-            custom_read_kwargs=self._read_csv_kwargs,
-            dataset_dirname=dataset_dirname,
-            downloads_dirname=downloads_dirname,
-            raw_dirname=raw_dirname,
-            preprocessed_dirname=preprocessed_dirname,
-            events_dirname=events_dirname,
-        )
-
-    @property
-    def path(self) -> Path:
-        """The path to the dataset directory.
-
-        The dataset path points to the dataset directory under the root path. Per default the
-        dataset directory name is equal to the class name.
-
-        Example
-        -------
-        The default behaviour is to locate the data set in a directory under the root path with the
-        same name as the class name:
-        >>> dataset = JuDo1000(root='/path/to/all/your/datasets')
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/all/your/datasets/JuDo1000')
-
-        You can specify an explicit dataset directory name:
-        >>> dataset = JuDo1000(
-        ...     root='/path/to/all/your/datasets',
-        ...     dataset_dirname='judo',
-        ... )
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/all/your/datasets/judo')
-
-        You can specify to use the root path to be the actual dataset directory:
-        >>> dataset = JuDo1000(root='/path/to/judo/dataset', dataset_dirname='.')
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/judo/dataset')
-        """
-        return super().path
diff --git a/src/pymovements/datasets/public_dataset.py b/src/pymovements/datasets/public_dataset.py
index 8ec0963..f077041 100644
--- a/src/pymovements/datasets/public_dataset.py
+++ b/src/pymovements/datasets/public_dataset.py
@@ -21,7 +21,8 @@
 from __future__ import annotations
 
 from abc import ABCMeta
-from abc import abstractmethod
+from dataclasses import dataclass
+from dataclasses import field
 from pathlib import Path
 from typing import Any
 from urllib.error import URLError
@@ -31,10 +32,77 @@ from pymovements.gaze.experiment import Experiment
 from pymovements.utils.archives import extract_archive
 from pymovements.utils.downloads import download_file
 
+PUBLIC_DATASETS: dict[str, type[PublicDatasetDefinition]] = {}
+
+
+def register_public_dataset(cls: type[PublicDatasetDefinition]) -> type[PublicDatasetDefinition]:
+    """Register a public dataset definition."""
+    PUBLIC_DATASETS[cls.__name__] = cls
+    return cls
+
+
+@dataclass
+class PublicDatasetDefinition:
+    """Definition to initialize a :py:class:`~pymovements.PublicDataset`.
+
+    Attributes
+    ----------
+    name : str
+        The name of the dataset.
+
+    mirrors : tuple[str, ...]
+        A tuple of mirrors of the dataset. Each entry must be of type `str` and end with a '/'.
+
+    resources : tuple[dict[str, str], ...]
+        A tuple of dataset resources. Each list entry must be a dictionary with the following keys:
+        - `resource`: The url suffix of the resource. This will be concatenated with the mirror.
+        - `filename`: The filename under which the file is saved as.
+        - `md5`: The MD5 checksum of the respective file.
+
+    experiment : Experiment
+        The experiment definition.
+
+    filename_regex : str
+        Regular expression which will be matched before trying to load the file. Namedgroups will
+        appear in the `fileinfo` dataframe.
+
+    filename_regex_dtypes : dict[str, type], optional
+        If named groups are present in the `filename_regex`, this makes it possible to cast specific
+        named groups to a particular datatype.
+
+    column_map : dict[str, str]
+        The keys are the columns to read, the values are the names to which they should be renamed.
+
+    custom_read_kwargs : dict[str, Any], optional
+        If specified, these keyword arguments will be passed to the file reading function.
+    """
+    mirrors: tuple[str, ...] = field(default_factory=tuple)
+
+    resources: tuple[dict[str, str], ...] = field(default_factory=tuple)
+
+    experiment: Experiment | None = None
+
+    filename_regex: str = '.*'
+
+    filename_regex_dtypes: dict[str, type] = field(default_factory=dict)
+
+    custom_read_kwargs: dict[str, Any] = field(default_factory=dict)
+
+    name: str = field(init=False)
+
+    column_map: dict[str, str] = field(default_factory=dict)
+
+    def __post_init__(self):
+        self.name = self.__class__.__name__
+
+        if len(self.column_map) > 0:
+            self.custom_read_kwargs['columns'] = list(self.column_map.keys())
+            self.custom_read_kwargs['new_columns'] = list(self.column_map.values())
+
 
 class PublicDataset(Dataset, metaclass=ABCMeta):
-    """Extends the `Dataset` base class with functionality for downloading and extracting public
-    datasets.
+    """Extends the :py:class:`~pymovements.Dataset` base class with functionality for downloading
+    and extracting public datasets.
 
     To implement this abstract base class for a new dataset, the attributes/properties `_mirrors`
     and `_resources` must be implemented.
@@ -42,11 +110,9 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
 
     def __init__(
             self,
+            definition: str | PublicDatasetDefinition | type[PublicDatasetDefinition],
+            *,
             root: str | Path,
-            experiment: Experiment | None = None,
-            filename_regex: str = '.*',
-            filename_regex_dtypes: dict[str, type] | None = None,
-            custom_read_kwargs: dict[str, Any] | None = None,
             dataset_dirname: str | None = None,
             downloads_dirname: str = 'downloads',
             raw_dirname: str = 'raw',
@@ -55,12 +121,6 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
     ):
         """Initialize the public dataset object.
 
-        If desired, dataset resources are downloaded with ``download=True`` and extracted with
-        ``extract=True``. To save space on your device you can remove the archive files after
-        successful extraction with ``remove_finished=True``.
-
-        Downloaded archives are automatically checked for integrity by comparing MD5 checksums.
-
         You can set up a custom directory structure by populating the particular dirname attributes.
         See :py:attr:`~pymovements.dataset.PublicDataset.dataset_dirname`,
         :py:attr:`~pymovements.dataset.PublicDataset.raw_dirname`,
@@ -72,18 +132,6 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
         ----------
         root : str, Path
             Path to the root directory of the dataset.
-        download : bool
-            Download all dataset resources.
-        experiment : Experiment
-            The experiment definition.
-        filename_regex : str
-            Regular expression which needs to be matched before trying to load the file. Named
-            groups will appear in the `fileinfo` dataframe.
-        filename_regex_dtypes : dict[str, type], optional
-            If named groups are present in the `filename_regex`, this makes it possible to cast
-            specific named groups to a particular datatype.
-        custom_read_kwargs : dict[str, Any], optional
-            If specified, these keyword arguments will be passed to the file reading function.
         dataset_dirname : str, optional
             Dataset directory name under root path. Can be `.` if dataset is located in root path.
             Default: `.`
@@ -101,20 +149,28 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
             Name of directory under dataset path that will be used to store event data. We advise
             the user to keep the event data separate from the original raw data. Default: `events`
         """
+        if isinstance(definition, str):
+            definition = PUBLIC_DATASETS[definition]()
+
+        if isinstance(definition, type):
+            definition = definition()
+
         if dataset_dirname is None:
-            dataset_dirname = self.__class__.__name__
+            dataset_dirname = definition.name
 
         super().__init__(
             root=root,
-            experiment=experiment,
-            filename_regex=filename_regex,
-            filename_regex_dtypes=filename_regex_dtypes,
-            custom_read_kwargs=custom_read_kwargs,
+            experiment=definition.experiment,
+            filename_regex=definition.filename_regex,
+            filename_regex_dtypes=definition.filename_regex_dtypes,
+            custom_read_kwargs=definition.custom_read_kwargs,
             dataset_dirname=dataset_dirname,
             raw_dirname=raw_dirname,
             preprocessed_dirname=preprocessed_dirname,
             events_dirname=events_dirname,
         )
+        self.mirrors = definition.mirrors
+        self.resources = definition.resources
         self.downloads_dirname = downloads_dirname
 
     def download(self, *, extract: bool = True, remove_finished: bool = False) -> Dataset:
@@ -122,6 +178,8 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
 
         This downloads all resources of the dataset. Per default this also extracts all archives
         into :py:meth:`Dataset.raw_rootpath`,
+        To save space on your device you can remove the archive files after
+        successful extraction with ``remove_finished=True``.
 
         If a corresponding file already exists in the local system, its checksum is calculated and
         checked against the expected checksum.
@@ -148,10 +206,10 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
         """
         self.raw_rootpath.mkdir(parents=True, exist_ok=True)
 
-        for resource in self._resources:
+        for resource in self.resources:
             success = False
 
-            for mirror in self._mirrors:
+            for mirror in self.mirrors:
 
                 url = f'{mirror}{resource["resource"]}'
 
@@ -198,7 +256,7 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
         """
         self.raw_rootpath.mkdir(parents=True, exist_ok=True)
 
-        for resource in self._resources:
+        for resource in self.resources:
             extract_archive(
                 source_path=self.downloads_rootpath / resource['filename'],
                 destination_path=self.raw_rootpath,
@@ -207,28 +265,6 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
             )
         return self
 
-    @property
-    @abstractmethod
-    def _mirrors(self):
-        """This attribute/property must provide a list of mirrors of the dataset.
-
-        Each entry should be of type `str` and end with a '/'.
-        """
-
-    @property
-    @abstractmethod
-    def _resources(self):
-        """This attribute must provide a list of dataset resources.
-
-        Each list entry should be a dictionary with the following keys:
-
-        - resource: The url suffix of the resource. This will be concatenated with the mirror.
-        - filename: The filename under which the file is saved as.
-        - md5: The MD5 checksum of the respective file.
-
-        All values should be of type string.
-        """
-
     @property
     def path(self) -> Path:
         """The path to the dataset directory.
@@ -239,9 +275,9 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
         Example
         -------
         Let's define a custom class first with one mirror and one resource.
-        >>> class CustomPublicDataset(PublicDataset):
-        ...     _mirrors = ['https://www.example.com/']
-        ...     _resources = [
+        >>> class CustomPublicDataset(PublicDatasetDefinition):
+        ...     mirrors = ['https://www.example.com/']
+        ...     resources = [
         ...         {
         ...             'resource': 'relative_path_to_resource',
         ...             'filename': 'example_dataset.zip',
@@ -251,12 +287,13 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
 
         The default behaviour is to locate the data set in a directory under the root path with the
         same name as the class name:
-        >>> dataset = CustomPublicDataset(root='/path/to/all/your/datasets')
+        >>> dataset = PublicDataset(CustomPublicDataset, root='/path/to/all/your/datasets')
         >>> dataset.path  # doctest: +SKIP
         Path('/path/to/all/your/datasets/CustomPublicDataset')
 
         You can specify an explicit dataset directory name:
-        >>> dataset = CustomPublicDataset(
+        >>> dataset = PublicDataset(
+        ...     CustomPublicDataset,
         ...     root='/path/to/all/your/datasets',
         ...     dataset_dirname='custom_dataset',
         ... )
@@ -264,7 +301,11 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
         Path('/path/to/all/your/datasets/custom_dataset')
 
         You can specify to use the root path to be the actual dataset directory:
-        >>> dataset = CustomPublicDataset(root='/path/to/your/dataset', dataset_dirname='.')
+        >>> dataset = PublicDataset(
+        ...     CustomPublicDataset,
+        ...     root='/path/to/your/datasets/',
+        ...     dataset_dirname='.',
+        ... )
         >>> dataset.path  # doctest: +SKIP
         Path('/path/to/your/dataset')
         """
@@ -279,22 +320,23 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
         Example
         -------
         Let's define a custom class first with one mirror and one resource.
-        >>> class CustomPublicDataset(PublicDataset):
-        ...     _mirrors = ['https://www.example.com/']
-        ...     _resources = [
+        >>> class CustomPublicDataset(PublicDatasetDefinition):
+        ...     mirrors: tuple[str] = ('https://www.example.com/', )
+        ...     resources: tuple[dict[str, str]] = (
         ...         {
         ...             'resource': 'relative_path_to_resource',
         ...             'filename': 'example_dataset.zip',
         ...             'md5': 'md5md5md5md5md5md5md5md5md5md5md',
         ...         },
-        ...     ]
+        ...     )
 
-        >>> dataset = CustomPublicDataset(root='/path/to/your/datasets/')
+        >>> dataset = PublicDataset(CustomPublicDataset, root='/path/to/your/datasets/')
         >>> dataset.downloads_rootpath  # doctest: +SKIP
         Path('/path/to/your/datasets/CustomPublicDataset/downloads')
 
         You can also explicitely specify the preprocessed directory name. The default is `raw`.
-        >>> dataset = CustomPublicDataset(
+        >>> dataset = PublicDataset(
+        ...     CustomPublicDataset,
         ...     root='/path/to/your/datasets/',
         ...     dataset_dirname='your_dataset',
         ...     downloads_dirname='your_raw_data',
@@ -306,7 +348,8 @@ class PublicDataset(Dataset, metaclass=ABCMeta):
         specify `.` as the directory name. We discourage this and advise the user to keep raw data
         and preprocessed data separated.
         ... dataset = Dataset(
-        >>> dataset = CustomPublicDataset(
+        >>> dataset = PublicDataset(
+        ...     CustomPublicDataset,
         ...     root='/path/to/your/datasets/',
         ...     dataset_dirname='your_dataset',
         ...     raw_dirname='.',
diff --git a/src/pymovements/datasets/toy_dataset.py b/src/pymovements/datasets/toy_dataset.py
deleted file mode 100644
index 10ce08e..0000000
--- a/src/pymovements/datasets/toy_dataset.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# Copyright (c) 2023 The pymovements Project Authors
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-"""This module provides an interface to the pymovements example toy dataset."""
-from __future__ import annotations
-
-from pathlib import Path
-
-from pymovements.datasets.public_dataset import PublicDataset
-from pymovements.gaze.experiment import Experiment
-
-
-class ToyDataset(PublicDataset):
-    """Example toy dataset.
-
-    This dataset includes monocular eye tracking data from a single participants in a single
-    session. Eye movements are recorded at a sampling frequency of 1000 Hz using an EyeLink Portable
-    Duo video-based eye tracker and are provided as pixel coordinates.
-
-    The participant is instructed to read 4 texts with 5 screens each.
-
-    Examples
-    --------
-    This will initialize your :py:class:`pymovements.PublicDataset` object, download its resources
-    and load the data into memory.
-
-    >>> dataset = ToyDataset(root='data/')
-    >>> dataset.download()  # doctest: +SKIP
-    >>> dataset.load()  # doctest: +SKIP
-    """
-    # pylint: disable=similarities
-    # The PublicDataset child classes potentially share code chunks for definitions.
-
-    _mirrors = [
-        'http://github.com/aeye-lab/pymovements-toy-dataset/zipball/',
-    ]
-
-    _resources = [
-        {
-            'resource': '6cb5d663317bf418cec0c9abe1dde5085a8a8ebd/',
-            'filename': 'pymovements-toy-dataset.zip',
-            'md5': '4da622457637a8181d86601fe17f3aa8',
-        },
-    ]
-
-    _experiment = Experiment(
-        screen_width_px=1280,
-        screen_height_px=1024,
-        screen_width_cm=38,
-        screen_height_cm=30.2,
-        distance_cm=68,
-        origin='lower left',
-        sampling_rate=1000,
-    )
-
-    _filename_regex = r'trial_(?P<text_id>\d+)_(?P<page_id>\d+).csv'
-
-    _filename_regex_dtypes = {
-        'text_id': int,
-        'page_id': int,
-    }
-
-    _column_map = {
-        'timestamp': 'time',
-        'x': 'x_right_pix',
-        'y': 'y_right_pix',
-    }
-
-    _read_csv_kwargs = {
-        'sep': '\t',
-        'columns': list(_column_map.keys()),
-        'new_columns': list(_column_map.values()),
-        'null_values': '-32768.00',
-    }
-
-    def __init__(
-            self,
-            root: str | Path,
-            dataset_dirname: str = 'ToyDataset',
-            downloads_dirname: str = 'downloads',
-            raw_dirname: str = 'raw',
-            preprocessed_dirname: str = 'preprocessed',
-            events_dirname: str = 'events',
-    ):
-        """Initialize the pymovements example toy dataset object.
-
-        If desired, dataset resources are downloaded with ``download=True`` and extracted with
-        ``extract=True``. To save space on your device you can remove the archive files after
-        successful extraction with ``remove_finished=True``.
-
-        Downloaded archives are automatically checked for integrity by comparing MD5 checksums.
-
-        You can set up a custom directory structure by populating the particular dirname attributes.
-        See :py:attr:`~pymovements.dataset.PublicDataset.dataset_dirname`,
-        :py:attr:`~pymovements.dataset.PublicDataset.raw_dirname`,
-        :py:attr:`~pymovements.dataset.PublicDataset.preprocessed_dirname` and
-        :py:attr:`~pymovements.dataset.PublicDataset.events_dirname` and
-        :py:attr:`~pymovements.dataset.PublicDataset.downloads_dirname` for details.
-
-        Parameters
-        ----------
-        root : str, Path
-            Path to the root directory of the dataset.
-        dataset_dirname : str, optional
-            Dataset directory name under root path. Can be `.` if dataset is located in root path.
-            Default: `.`
-        downloads_dirname : str, optional
-            Name of directory to store downloaded data.Default: `downloads`
-        raw_dirname ; str, optional
-            Name of directory under dataset path that contains raw data. Can be `.` if raw data is
-            located in dataset path. We advise the user to keep the original raw data separate from
-            the preprocessed / event data. Default: `raw`
-        preprocessed_dirname : str, optional
-            Name of directory under dataset path that will be used to store preprocessed data. We
-            advise the user to keep the preprocessed data separate from the original raw data.
-            Default: `preprocessed`
-        events_dirname : str, optional
-            Name of directory under dataset path that will be used to store event data. We advise
-            the user to keep the event data separate from the original raw data. Default: `events`
-        """
-        super().__init__(
-            root=root,
-            experiment=self._experiment,
-            filename_regex=self._filename_regex,
-            filename_regex_dtypes=self._filename_regex_dtypes,
-            custom_read_kwargs=self._read_csv_kwargs,
-            dataset_dirname=dataset_dirname,
-            downloads_dirname=downloads_dirname,
-            raw_dirname=raw_dirname,
-            preprocessed_dirname=preprocessed_dirname,
-            events_dirname=events_dirname,
-        )
-
-    @property
-    def path(self) -> Path:
-        """The path to the dataset directory.
-
-        The dataset path points to the dataset directory under the root path. Per default the
-        dataset directory name is equal to the class name.
-
-        Example
-        -------
-        The default behaviour is to locate the data set in a directory under the root path with the
-        same name as the class name:
-        >>> dataset = ToyDataset(root='/path/to/all/your/datasets')
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/all/your/datasets/ToyDataset')
-
-        You can specify an explicit dataset directory name:
-        >>> dataset = ToyDataset(
-        ...     root='/path/to/all/your/datasets',
-        ...     dataset_dirname='toy',
-        ... )
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/all/your/datasets/toy')
-
-        You can specify to use the root path to be the actual dataset directory:
-        >>> dataset = ToyDataset(root='/path/to/toy/dataset', dataset_dirname='.')
-        >>> dataset.path  # doctest: +SKIP
-        Path('/path/to/toy/dataset')
-        """
-        return super().path


Test patch:
diff --git a/tests/datasets/definitions/public_dataset_definitions_test.py b/tests/datasets/definitions/public_dataset_definitions_test.py
new file mode 100644
index 0000000..7783b03
--- /dev/null
+++ b/tests/datasets/definitions/public_dataset_definitions_test.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Test public dataset definitions"""
+import pytest
+
+from pymovements.datasets import definitions
+from pymovements.datasets.public_dataset import PUBLIC_DATASETS
+
+
+@pytest.mark.parametrize(
+    ('definition_class', 'dataset_name'),
+    [
+        pytest.param(definitions.ToyDataset, 'ToyDataset', id='ToyDataset'),
+        pytest.param(definitions.GazeBase, 'GazeBase', id='GazeBase'),
+        pytest.param(definitions.JuDo1000, 'JuDo1000', id='JuDo1000'),
+    ],
+)
+def test_public_dataset_registered(definition_class, dataset_name):
+    assert dataset_name in PUBLIC_DATASETS
+    assert PUBLIC_DATASETS[dataset_name] == definition_class
+    assert PUBLIC_DATASETS[dataset_name]().name == dataset_name
+
+
+@pytest.mark.parametrize(
+    'dataset_definition_class',
+    [
+        pytest.param(definitions.ToyDataset, id='ToyDataset'),
+        pytest.param(definitions.GazeBase, id='GazeBase'),
+        pytest.param(definitions.JuDo1000, id='JuDo1000'),
+    ],
+)
+def test_public_dataset_registered_correct_attributes(dataset_definition_class):
+    dataset_definition = dataset_definition_class()
+
+    registered_definition = PUBLIC_DATASETS[dataset_definition.name]()
+
+    assert dataset_definition.mirrors == registered_definition.mirrors
+    assert dataset_definition.resources == registered_definition.resources
+    assert dataset_definition.experiment == registered_definition.experiment
+    assert dataset_definition.filename_regex == registered_definition.filename_regex
+    assert dataset_definition.filename_regex_dtypes == registered_definition.filename_regex_dtypes
+    assert dataset_definition.custom_read_kwargs == registered_definition.custom_read_kwargs
diff --git a/tests/datasets/gazebase_test.py b/tests/datasets/gazebase_test.py
index 53ad744..d08a8de 100644
--- a/tests/datasets/gazebase_test.py
+++ b/tests/datasets/gazebase_test.py
@@ -22,7 +22,8 @@ from pathlib import Path
 
 import pytest
 
-from pymovements.datasets.gazebase import GazeBase
+from pymovements.datasets.definitions.gazebase import GazeBase
+from pymovements.datasets.public_dataset import PublicDataset
 
 
 @pytest.mark.parametrize(
@@ -63,7 +64,7 @@ from pymovements.datasets.gazebase import GazeBase
     ],
 )
 def test_paths(init_kwargs, expected_paths):
-    dataset = GazeBase(**init_kwargs)
+    dataset = PublicDataset(GazeBase, **init_kwargs)
 
     assert dataset.root == expected_paths['root']
     assert dataset.path == expected_paths['path']
diff --git a/tests/datasets/judo1000_test.py b/tests/datasets/judo1000_test.py
index f569949..759bf1a 100644
--- a/tests/datasets/judo1000_test.py
+++ b/tests/datasets/judo1000_test.py
@@ -22,7 +22,8 @@ from pathlib import Path
 
 import pytest
 
-from pymovements.datasets.judo1000 import JuDo1000
+from pymovements.datasets.definitions.judo1000 import JuDo1000
+from pymovements.datasets.public_dataset import PublicDataset
 
 
 @pytest.mark.parametrize(
@@ -63,7 +64,7 @@ from pymovements.datasets.judo1000 import JuDo1000
     ],
 )
 def test_paths(init_kwargs, expected_paths):
-    dataset = JuDo1000(**init_kwargs)
+    dataset = PublicDataset(JuDo1000, **init_kwargs)
 
     assert dataset.root == expected_paths['root']
     assert dataset.path == expected_paths['path']
diff --git a/tests/datasets/public_dataset_test.py b/tests/datasets/public_dataset_test.py
index 1fd30b6..a435234 100644
--- a/tests/datasets/public_dataset_test.py
+++ b/tests/datasets/public_dataset_test.py
@@ -18,31 +18,38 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 """Test all functionality in pymovements.datasets.public_dataset."""
+from __future__ import annotations
+
+from dataclasses import dataclass
 from pathlib import Path
 from unittest import mock
 
 import pytest
 
 from pymovements.datasets.public_dataset import PublicDataset
+from pymovements.datasets.public_dataset import PublicDatasetDefinition
+from pymovements.datasets.public_dataset import register_public_dataset
+
 
+@pytest.fixture(name='dataset_definition')
+def dataset_definition_fixture():
+    @dataclass
+    @register_public_dataset
+    class CustomPublicDataset(PublicDatasetDefinition):
+        mirrors: tuple[str, ...] = (
+            'https://example.com/',
+            'https://another_example.com/',
+        )
 
-@pytest.fixture(name='dataset')
-def custom_dataset(tmp_path):
-    class CustomPublicDataset(PublicDataset):
-        _mirrors = ['https://example.com/', 'https://another_example.com/']
-        _resources = [
+        resources: tuple[dict[str, str], ...] = (
             {
                 'resource': 'test.gz.tar',
                 'filename': 'test.gz.tar',
                 'md5': '52bbf03a7c50ee7152ccb9d357c2bb30',
             },
-        ]
+        )
 
-    return CustomPublicDataset(
-        root=tmp_path,
-        dataset_dirname='',
-        downloads_dirname='',
-    )
+    return CustomPublicDataset()
 
 
 @pytest.mark.parametrize(
@@ -82,18 +89,8 @@ def custom_dataset(tmp_path):
         ),
     ],
 )
-def test_paths(init_kwargs, expected_paths):
-    class CustomPublicDataset(PublicDataset):
-        _mirrors = ['https://www.example.com/']
-        _resources = [
-            {
-                'resource': 'relative_path_to_resource',
-                'filename': 'example_dataset.zip',
-                'md5': 'md5md5md5md5md5md5md5md5md5md5md',
-            },
-        ]
-
-    dataset = CustomPublicDataset(**init_kwargs)
+def test_paths(init_kwargs, expected_paths, dataset_definition):
+    dataset = PublicDataset(dataset_definition, **init_kwargs)
 
     assert dataset.root == expected_paths['root']
     assert dataset.path == expected_paths['path']
@@ -101,16 +98,47 @@ def test_paths(init_kwargs, expected_paths):
 
 
 @mock.patch('pymovements.datasets.public_dataset.download_file')
-def test_dataset_download_fails(mock_download_file, tmp_path, dataset):
+def test_dataset_download_both_mirrors_fail(mock_download_file, tmp_path, dataset_definition):
     mock_download_file.side_effect = OSError()
 
+    dataset = PublicDataset(dataset_definition, root=tmp_path, dataset_dirname='.')
+
     with pytest.raises(RuntimeError):
         dataset.download()
 
     mock_download_file.assert_has_calls([
         mock.call(
             url='https://example.com/test.gz.tar',
-            dirpath=tmp_path,
+            dirpath=tmp_path / 'downloads',
+            filename='test.gz.tar',
+            md5='52bbf03a7c50ee7152ccb9d357c2bb30',
+        ),
+        mock.call(
+            url='https://another_example.com/test.gz.tar',
+            dirpath=tmp_path / 'downloads',
+            filename='test.gz.tar',
+            md5='52bbf03a7c50ee7152ccb9d357c2bb30',
+        ),
+    ])
+
+
+@mock.patch('pymovements.datasets.public_dataset.download_file')
+def test_dataset_download_first_mirror_fails(mock_download_file, tmp_path, dataset_definition):
+    mock_download_file.side_effect = [OSError(), None]
+
+    dataset = PublicDataset(dataset_definition, root=tmp_path, dataset_dirname='.')
+    dataset.download(extract=False)
+
+    mock_download_file.assert_has_calls([
+        mock.call(
+            url='https://example.com/test.gz.tar',
+            dirpath=tmp_path / 'downloads',
+            filename='test.gz.tar',
+            md5='52bbf03a7c50ee7152ccb9d357c2bb30',
+        ),
+        mock.call(
+            url='https://another_example.com/test.gz.tar',
+            dirpath=tmp_path / 'downloads',
             filename='test.gz.tar',
             md5='52bbf03a7c50ee7152ccb9d357c2bb30',
         ),
@@ -118,16 +146,18 @@ def test_dataset_download_fails(mock_download_file, tmp_path, dataset):
 
 
 @mock.patch('pymovements.datasets.public_dataset.download_file')
-def test_dataset_download_file_not_found(mock_download_file, tmp_path, dataset):
+def test_dataset_download_file_not_found(mock_download_file, tmp_path, dataset_definition):
     mock_download_file.side_effect = RuntimeError()
 
+    dataset = PublicDataset(dataset_definition, root=tmp_path, dataset_dirname='.')
+
     with pytest.raises(RuntimeError):
         dataset.download()
 
     mock_download_file.assert_has_calls([
         mock.call(
             url='https://example.com/test.gz.tar',
-            dirpath=tmp_path,
+            dirpath=tmp_path / 'downloads',
             filename='test.gz.tar',
             md5='52bbf03a7c50ee7152ccb9d357c2bb30',
         ),
@@ -135,15 +165,16 @@ def test_dataset_download_file_not_found(mock_download_file, tmp_path, dataset):
 
 
 @mock.patch('pymovements.datasets.public_dataset.download_file')
-def test_dataset_download_no_extract(mock_download_file, tmp_path, dataset):
+def test_dataset_download_no_extract(mock_download_file, tmp_path, dataset_definition):
     mock_download_file.return_value = 'path'
 
+    dataset = PublicDataset(dataset_definition, root=tmp_path, dataset_dirname='.')
     dataset.download(extract=False)
 
     mock_download_file.assert_has_calls([
         mock.call(
             url='https://example.com/test.gz.tar',
-            dirpath=tmp_path,
+            dirpath=tmp_path / 'downloads',
             filename='test.gz.tar',
             md5='52bbf03a7c50ee7152ccb9d357c2bb30',
         ),
@@ -154,15 +185,16 @@ def test_dataset_download_no_extract(mock_download_file, tmp_path, dataset):
 def test_dataset_extract_remove_finished_true(
         mock_extract_archive,
         tmp_path,
-        dataset,
+        dataset_definition,
 ):
     mock_extract_archive.return_value = 'path'
 
+    dataset = PublicDataset(dataset_definition, root=tmp_path, dataset_dirname='.')
     dataset.extract(remove_finished=True)
 
     mock_extract_archive.assert_has_calls([
         mock.call(
-            source_path=tmp_path / 'test.gz.tar',
+            source_path=tmp_path / 'downloads' / 'test.gz.tar',
             destination_path=tmp_path / 'raw',
             recursive=True,
             remove_finished=True,
@@ -174,15 +206,16 @@ def test_dataset_extract_remove_finished_true(
 def test_dataset_extract_remove_finished_false(
         mock_extract_archive,
         tmp_path,
-        dataset,
+        dataset_definition,
 ):
     mock_extract_archive.return_value = 'path'
 
+    dataset = PublicDataset(dataset_definition, root=tmp_path, dataset_dirname='.')
     dataset.extract()
 
     mock_extract_archive.assert_has_calls([
         mock.call(
-            source_path=tmp_path / 'test.gz.tar',
+            source_path=tmp_path / 'downloads' / 'test.gz.tar',
             destination_path=tmp_path / 'raw',
             recursive=True,
             remove_finished=False,
@@ -192,21 +225,24 @@ def test_dataset_extract_remove_finished_false(
 
 @mock.patch('pymovements.datasets.public_dataset.download_file')
 @mock.patch('pymovements.datasets.public_dataset.extract_archive')
-def test_custom_dataset_download_default_extract(mock_extract, mock_download, tmp_path):
+def test_dataset_download_default_extract(
+        mock_extract, mock_download, tmp_path, dataset_definition,
+):
     mock_extract.return_value = None
     mock_download.return_value = None
 
-    class CustomPublicDataset(PublicDataset):
-        _mirrors = ['https://example.com/']
-        _resources = [
-            {
-                'resource': 'test.gz.tar',
-                'filename': 'test.gz.tar',
-                'md5': '52bbf03a7c50ee7152ccb9d357c2bb30',
-            },
-        ]
-
-    CustomPublicDataset(root=tmp_path).download()
+    PublicDataset(dataset_definition, root=tmp_path).download()
 
     mock_download.assert_called_once()
     mock_extract.assert_called_once()
+
+
+def test_public_dataset_registered_correct_attributes(tmp_path, dataset_definition):
+    dataset = PublicDataset('CustomPublicDataset', root=tmp_path)
+
+    assert dataset.mirrors == dataset_definition.mirrors
+    assert dataset.resources == dataset_definition.resources
+    assert dataset.experiment == dataset_definition.experiment
+    assert dataset._filename_regex == dataset_definition.filename_regex
+    assert dataset._filename_regex_dtypes == dataset_definition.filename_regex_dtypes
+    assert dataset._custom_read_kwargs == dataset_definition.custom_read_kwargs
diff --git a/tests/datasets/toy_dataset_test.py b/tests/datasets/toy_dataset_test.py
index 64cb43a..a5a6979 100644
--- a/tests/datasets/toy_dataset_test.py
+++ b/tests/datasets/toy_dataset_test.py
@@ -22,7 +22,8 @@ from pathlib import Path
 
 import pytest
 
-from pymovements.datasets.toy_dataset import ToyDataset
+from pymovements.datasets.definitions.toy_dataset import ToyDataset
+from pymovements.datasets.public_dataset import PublicDataset
 
 
 @pytest.mark.parametrize(
@@ -63,7 +64,7 @@ from pymovements.datasets.toy_dataset import ToyDataset
     ],
 )
 def test_paths(init_kwargs, expected_paths):
-    dataset = ToyDataset(**init_kwargs)
+    dataset = PublicDataset(ToyDataset, **init_kwargs)
 
     assert dataset.root == expected_paths['root']
     assert dataset.path == expected_paths['path']