diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pyproject.toml b/pyproject.toml
index d4e1aef6e781053509e570e7f57a0808d6f51e91..faf01b7944be5b56adc4d3e5b2383117a31e5a2d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -476,7 +476,7 @@ convention = "numpy"
 
 [tool.ruff.lint.per-file-ignores]
 "helpers/*.py" = ["T201", "D103"]
-"tests/*.py" = ["D", "E501"]
+"**/tests/*.py" = ["D", "E501"]
 "doc/conf.py" = ["D"]
 "**/scripts/*.py" = ["E501"]
 
diff --git a/tests/data/histograms/models/histograms_alexnet_montgomery_default.json b/src/mednet/libs/classification/tests/data/histograms/models/histograms_alexnet_montgomery_default.json
similarity index 100%
rename from tests/data/histograms/models/histograms_alexnet_montgomery_default.json
rename to src/mednet/libs/classification/tests/data/histograms/models/histograms_alexnet_montgomery_default.json
diff --git a/tests/data/histograms/models/histograms_densenet-121_montgomery_default.json b/src/mednet/libs/classification/tests/data/histograms/models/histograms_densenet-121_montgomery_default.json
similarity index 100%
rename from tests/data/histograms/models/histograms_densenet-121_montgomery_default.json
rename to src/mednet/libs/classification/tests/data/histograms/models/histograms_densenet-121_montgomery_default.json
diff --git a/tests/data/histograms/models/histograms_pasa_montgomery_default.json b/src/mednet/libs/classification/tests/data/histograms/models/histograms_pasa_montgomery_default.json
similarity index 100%
rename from tests/data/histograms/models/histograms_pasa_montgomery_default.json
rename to src/mednet/libs/classification/tests/data/histograms/models/histograms_pasa_montgomery_default.json
diff --git a/tests/data/histograms/raw_data/histograms_hivtb_fold_0.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_hivtb_fold_0.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_hivtb_fold_0.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_hivtb_fold_0.json
diff --git a/tests/data/histograms/raw_data/histograms_indian_default.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_indian_default.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_indian_default.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_indian_default.json
diff --git a/tests/data/histograms/raw_data/histograms_montgomery_default.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_montgomery_default.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_montgomery_default.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_montgomery_default.json
diff --git a/tests/data/histograms/raw_data/histograms_montgomery_preprocessed_default.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_montgomery_preprocessed_default.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_montgomery_preprocessed_default.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_montgomery_preprocessed_default.json
diff --git a/tests/data/histograms/raw_data/histograms_nih_cxr14_default.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_nih_cxr14_default.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_nih_cxr14_default.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_nih_cxr14_default.json
diff --git a/tests/data/histograms/raw_data/histograms_padchest_idiap.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_padchest_idiap.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_padchest_idiap.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_padchest_idiap.json
diff --git a/tests/data/histograms/raw_data/histograms_shenzhen_default.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_shenzhen_default.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_shenzhen_default.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_shenzhen_default.json
diff --git a/tests/data/histograms/raw_data/histograms_tbpoc_fold_0.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_tbpoc_fold_0.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_tbpoc_fold_0.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_tbpoc_fold_0.json
diff --git a/tests/data/histograms/raw_data/histograms_tbx11k_v1_fold_0.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_tbx11k_v1_fold_0.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_tbx11k_v1_fold_0.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_tbx11k_v1_fold_0.json
diff --git a/tests/data/histograms/raw_data/histograms_tbx11k_v2_fold_0.json b/src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_tbx11k_v2_fold_0.json
similarity index 100%
rename from tests/data/histograms/raw_data/histograms_tbx11k_v2_fold_0.json
rename to src/mednet/libs/classification/tests/data/histograms/raw_data/histograms_tbx11k_v2_fold_0.json
diff --git a/src/mednet/libs/classification/tests/data/lfs/.gitattributes b/src/mednet/libs/classification/tests/data/lfs/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..ebbbf5f9ef5538de427dbe579433ef3349c20cad
--- /dev/null
+++ b/src/mednet/libs/classification/tests/data/lfs/.gitattributes
@@ -0,0 +1,6 @@
+_test_densenetrs_checkpoint.pth filter=lfs diff=lfs merge=lfs -text
+_test_fpasa_checkpoint.pth filter=lfs diff=lfs merge=lfs -text
+_test_logreg_checkpoint.pth filter=lfs diff=lfs merge=lfs -text
+_test_signstotb_checkpoint.pth filter=lfs diff=lfs merge=lfs -text
+_testdb.zip filter=lfs diff=lfs merge=lfs -text
+pasa.pth filter=lfs diff=lfs merge=lfs -text
diff --git a/src/mednet/libs/classification/tests/data/lfs/.gitignore b/src/mednet/libs/classification/tests/data/lfs/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b25c15b81fae06e1c55946ac6270bfdb293870e8
--- /dev/null
+++ b/src/mednet/libs/classification/tests/data/lfs/.gitignore
@@ -0,0 +1 @@
+*~
diff --git a/src/mednet/libs/classification/tests/data/lfs/README.md b/src/mednet/libs/classification/tests/data/lfs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..bc66bf5e69eb8db14bb357d1c3278697f7491e2b
--- /dev/null
+++ b/src/mednet/libs/classification/tests/data/lfs/README.md
@@ -0,0 +1,14 @@
+# Assets for Testing biosignal/software/mednet>
+
+This package contains test unit assets used by the test suit of
+biosignal/software/mednet>.
+
+
+## Updating
+
+To update the contents of this package, use [git-lfs](https://git-lfs.com),
+following the workflow described at the [GitLab support
+page](https://docs.gitlab.com/ee/topics/git/lfs/).
+
+Cloning and updating the repository, in particular, works the same as before
+and should not impose any workflow changes.
diff --git a/src/mednet/libs/classification/tests/data/lfs/models/logreg.ckpt b/src/mednet/libs/classification/tests/data/lfs/models/logreg.ckpt
new file mode 100644
index 0000000000000000000000000000000000000000..b8609b572753ff637cac6edb39d643ec683abebb
Binary files /dev/null and b/src/mednet/libs/classification/tests/data/lfs/models/logreg.ckpt differ
diff --git a/src/mednet/libs/classification/tests/data/lfs/models/signstotb.ckpt b/src/mednet/libs/classification/tests/data/lfs/models/signstotb.ckpt
new file mode 100644
index 0000000000000000000000000000000000000000..59ea4595166b871e031c9664c1e0581081f3412e
Binary files /dev/null and b/src/mednet/libs/classification/tests/data/lfs/models/signstotb.ckpt differ
diff --git a/tests/data/mednet.toml b/src/mednet/libs/classification/tests/data/mednet.toml
similarity index 100%
rename from tests/data/mednet.toml
rename to src/mednet/libs/classification/tests/data/mednet.toml
diff --git a/src/mednet/libs/classification/tests/data/test_predictions.csv b/src/mednet/libs/classification/tests/data/test_predictions.csv
new file mode 100644
index 0000000000000000000000000000000000000000..5de618e0b3f567a982d8a63341a6741d738274bc
--- /dev/null
+++ b/src/mednet/libs/classification/tests/data/test_predictions.csv
@@ -0,0 +1,10 @@
+filename,likelihood,ground_truth
+file1,"[6.1538161e-01 1.4814811e-03 6.5176686e-05 1.0000000e+00 5.1068876e-01
+ 1.8108148e-04 3.6165615e-01 1.4568567e-01 4.8867718e-05 5.0666117e-05
+ 5.1114771e-07 1.3583761e-01 6.0767888e-11 1.5170315e-08]",[1.]
+file2,"[6.3864078e-04 3.6866156e-03 8.4878616e-08 6.3316641e-01 4.4661388e-01
+ 7.6667184e-04 1.1361861e-03 4.6111313e-03 4.3461104e-04 1.1185581e-06
+ 1.1631314e-06 3.7814886e-06 6.1658076e-11 3.5506051e-08]",[0.]
+file3,"[1.3780616e-05 1.8464373e-05 1.4054011e-07 1.6511037e-03 7.0664110e-01
+ 6.0737631e-03 7.3566751e-03 3.0668571e-04 3.1133456e-05 5.0116336e-07
+ 1.5665150e-04 1.0710500e-08 4.1036647e-06 1.1140607e-07]",[1.]
diff --git a/src/mednet/libs/classification/tests/data/test_vis_metrics.csv b/src/mednet/libs/classification/tests/data/test_vis_metrics.csv
new file mode 100644
index 0000000000000000000000000000000000000000..1a144f21b6f21693d56be19ef301c240f30398e3
--- /dev/null
+++ b/src/mednet/libs/classification/tests/data/test_vis_metrics.csv
@@ -0,0 +1,6 @@
+Image,MoRF,LeRF,Combined Score ((LeRF-MoRF) / 2),IoU,IoDA,propEnergy,ASF
+tb0004.png,1,2,3,4,5,6,7
+tb0006.png,2,3,4,5,6,7,8
+tb0009.png,1,2,3,4,5,6,7
+tb0014.png,2,3,4,5,6,7,8
+tb0015.png,1,2,3,4,5,6,7
diff --git a/tests/test_cli.py b/src/mednet/libs/classification/tests/test_cli_classification.py
similarity index 96%
rename from tests/test_cli.py
rename to src/mednet/libs/classification/tests/test_cli_classification.py
index bb3c1ceddfa0c09c27b093eda499d702aea85564..f735ae171bfd97dc78b60049423eaa56f650a519 100644
--- a/tests/test_cli.py
+++ b/src/mednet/libs/classification/tests/test_cli_classification.py
@@ -40,26 +40,6 @@ def _check_help(entry_point):
     assert result.output.startswith("Usage:")
 
 
-def test_info_help():
-    from mednet.scripts.info import info
-
-    _check_help(info)
-
-
-def test_info():
-    from mednet.scripts.info import info
-
-    runner = CliRunner()
-    result = runner.invoke(info)
-    _assert_exit_0(result)
-    assert "platform:" in result.output
-    assert "accelerators:" in result.output
-    assert "version:" in result.output
-    assert "configured databases:" in result.output
-    assert "dependencies:" in result.output
-    assert "python:" in result.output
-
-
 def test_config_help():
     from mednet.libs.classification.scripts.config import config
 
diff --git a/tests/test_evaluator.py b/src/mednet/libs/classification/tests/test_evaluator.py
similarity index 100%
rename from tests/test_evaluator.py
rename to src/mednet/libs/classification/tests/test_evaluator.py
diff --git a/tests/test_hivtb.py b/src/mednet/libs/classification/tests/test_hivtb.py
similarity index 100%
rename from tests/test_hivtb.py
rename to src/mednet/libs/classification/tests/test_hivtb.py
diff --git a/tests/test_indian.py b/src/mednet/libs/classification/tests/test_indian.py
similarity index 100%
rename from tests/test_indian.py
rename to src/mednet/libs/classification/tests/test_indian.py
diff --git a/tests/test_montgomery.py b/src/mednet/libs/classification/tests/test_montgomery.py
similarity index 100%
rename from tests/test_montgomery.py
rename to src/mednet/libs/classification/tests/test_montgomery.py
diff --git a/tests/test_montgomery_shenzhen.py b/src/mednet/libs/classification/tests/test_montgomery_shenzhen.py
similarity index 100%
rename from tests/test_montgomery_shenzhen.py
rename to src/mednet/libs/classification/tests/test_montgomery_shenzhen.py
diff --git a/tests/test_montgomery_shenzhen_indian.py b/src/mednet/libs/classification/tests/test_montgomery_shenzhen_indian.py
similarity index 100%
rename from tests/test_montgomery_shenzhen_indian.py
rename to src/mednet/libs/classification/tests/test_montgomery_shenzhen_indian.py
diff --git a/tests/test_montgomery_shenzhen_indian_padchest.py b/src/mednet/libs/classification/tests/test_montgomery_shenzhen_indian_padchest.py
similarity index 100%
rename from tests/test_montgomery_shenzhen_indian_padchest.py
rename to src/mednet/libs/classification/tests/test_montgomery_shenzhen_indian_padchest.py
diff --git a/tests/test_montgomery_shenzhen_indian_tbx11k.py b/src/mednet/libs/classification/tests/test_montgomery_shenzhen_indian_tbx11k.py
similarity index 100%
rename from tests/test_montgomery_shenzhen_indian_tbx11k.py
rename to src/mednet/libs/classification/tests/test_montgomery_shenzhen_indian_tbx11k.py
diff --git a/tests/test_nih_cxr14.py b/src/mednet/libs/classification/tests/test_nih_cxr14.py
similarity index 100%
rename from tests/test_nih_cxr14.py
rename to src/mednet/libs/classification/tests/test_nih_cxr14.py
diff --git a/tests/test_nih_cxr14_padchest.py b/src/mednet/libs/classification/tests/test_nih_cxr14_padchest.py
similarity index 100%
rename from tests/test_nih_cxr14_padchest.py
rename to src/mednet/libs/classification/tests/test_nih_cxr14_padchest.py
diff --git a/tests/test_padchest.py b/src/mednet/libs/classification/tests/test_padchest.py
similarity index 100%
rename from tests/test_padchest.py
rename to src/mednet/libs/classification/tests/test_padchest.py
diff --git a/tests/test_saliencymap_interpretability.py b/src/mednet/libs/classification/tests/test_saliencymap_interpretability.py
similarity index 100%
rename from tests/test_saliencymap_interpretability.py
rename to src/mednet/libs/classification/tests/test_saliencymap_interpretability.py
diff --git a/tests/test_shenzhen.py b/src/mednet/libs/classification/tests/test_shenzhen.py
similarity index 100%
rename from tests/test_shenzhen.py
rename to src/mednet/libs/classification/tests/test_shenzhen.py
diff --git a/tests/test_summary.py b/src/mednet/libs/classification/tests/test_summary.py
similarity index 100%
rename from tests/test_summary.py
rename to src/mednet/libs/classification/tests/test_summary.py
diff --git a/tests/test_tbpoc.py b/src/mednet/libs/classification/tests/test_tbpoc.py
similarity index 100%
rename from tests/test_tbpoc.py
rename to src/mednet/libs/classification/tests/test_tbpoc.py
diff --git a/tests/test_tbx11k.py b/src/mednet/libs/classification/tests/test_tbx11k.py
similarity index 100%
rename from tests/test_tbx11k.py
rename to src/mednet/libs/classification/tests/test_tbx11k.py
diff --git a/tests/test_visceral.py b/src/mednet/libs/classification/tests/test_visceral.py
similarity index 90%
rename from tests/test_visceral.py
rename to src/mednet/libs/classification/tests/test_visceral.py
index 9743577e1dc68f819c556177dc2e77dfa616baeb..77d0e94bf2d3b624497e675284d29a98c9439540 100644
--- a/tests/test_visceral.py
+++ b/src/mednet/libs/classification/tests/test_visceral.py
@@ -25,7 +25,7 @@ def test_protocol_consistency(
     split: str,
     lenghts: dict[str, int],
 ):
-    from mednet.data.split import make_split
+    from mednet.libs.common.data.split import make_split
 
     database_checkers.check_split(
         make_split("mednet.config.data.visceral", f"{split}.json"),
@@ -37,7 +37,7 @@ def test_protocol_consistency(
 
 @pytest.mark.skip_if_rc_var_not_set("datadir.visceral")
 def test_database_check():
-    from mednet.scripts.database import check
+    from mednet.libs.common.scripts.database import check
 
     runner = CliRunner()
     result = runner.invoke(check, ["visceral"])
diff --git a/src/mednet/libs/common/scripts/upload.py b/src/mednet/libs/common/scripts/upload.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccca1ce840f68010bbdb93658733a90cdf2b068c
--- /dev/null
+++ b/src/mednet/libs/common/scripts/upload.py
@@ -0,0 +1,211 @@
+# SPDX-FileCopyrightText: Copyright © 2023 Idiap Research Institute <contact@idiap.ch>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import pathlib
+
+import click
+from clapper.click import ResourceOption, verbosity_option
+from clapper.logging import setup
+
+from .click import ConfigCommand
+
+logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
+
+
+@click.command(
+    entry_point_group="mednet.config",
+    cls=ConfigCommand,
+    epilog="""Examples:
+
+1. Upload an existing experiment result from a path it resides on (with a default experiment name as {model-name}_{database-name} and a default run name as {date-time}):
+
+   .. code:: sh
+
+      mednet upload --experiment-folder=/path/to/results
+
+2. Upload an existing experiment result with an experiment name:
+
+   .. code:: sh
+
+      mednet upload --experiment-folder=/path/to/results --experiment-name=exp-pasa_mc
+
+3. Upload an existing experiment result with a run name:
+
+   .. code:: sh
+
+      mednet upload --experiment-folder=/path/to/results --run-name=run-1
+
+4. Upload an existing experiment result with defining a size limit of 20MB for each file:
+
+   .. code:: sh
+
+      mednet upload --experiment-folder=/path/to/results --upload-limit-mb=20
+
+""",
+)
+@click.option(
+    "--project-path",
+    "-p",
+    help="Path to the project where to upload model entries",
+    required=True,
+    type=str,
+    default="biosignal/software/mednet",
+    show_default=True,
+    cls=ResourceOption,
+)
+@click.option(
+    "--experiment-folder",
+    "-f",
+    help="Directory in which to upload results from",
+    required=True,
+    type=click.Path(
+        file_okay=False,
+        dir_okay=True,
+        path_type=pathlib.Path,
+    ),
+    default="results",
+    show_default=True,
+    cls=ResourceOption,
+)
+@click.option(
+    "--experiment-name",
+    "-e",
+    help='A string indicating the experiment name (e.g. "exp-pasa-mc" or "exp-densenet-mc-ch")',
+    cls=ResourceOption,
+)
+@click.option(
+    "--run-name",
+    "-r",
+    help='A string indicating the run name (e.g. "run-1")',
+    cls=ResourceOption,
+)
+@click.option(
+    "--upload-limit-mb",
+    "-l",
+    help="Maximim upload size in MB (set to 0 for no limit).",
+    show_default=True,
+    required=True,
+    default=10,
+    type=click.IntRange(min=0),
+    cls=ResourceOption,
+)
+@verbosity_option(logger=logger, cls=ResourceOption, expose_value=False)
+def upload(
+    project_path: str,
+    experiment_folder: pathlib.Path,
+    experiment_name: str,
+    run_name: str,
+    upload_limit_mb: int,
+    **_,  # ignored
+) -> None:  # numpydoc ignore=PR01
+    """Upload results from an experiment folder to GitLab's MLFlow server."""
+
+    import json
+    import os
+    import tempfile
+
+    import mlflow
+    from mednet.libs.common.utils.checkpointer import (
+        get_checkpoint_to_run_inference,
+    )
+    from mednet.libs.common.utils.gitlab import (
+        gitlab_instance_and_token,
+        sanitize_filename,
+        size_in_mb,
+    )
+
+    logger.info(
+        "Retrieving GitLab credentials for access to hosted MLFlow server..."
+    )
+    gitlab, token = gitlab_instance_and_token()
+    project = gitlab.projects.get(project_path)
+    os.environ["MLFLOW_TRACKING_TOKEN"] = token
+    os.environ["MLFLOW_TRACKING_URI"] = (
+        gitlab.api_url + f"/projects/{project.id}/ml/mlflow"
+    )
+
+    # get train files
+    train_folder = experiment_folder / "model"
+    train_meta_file = train_folder / "meta.json"
+    train_log_file = train_folder / "trainlog.pdf"
+    train_model_file = get_checkpoint_to_run_inference(train_folder)
+    train_files = [train_meta_file, train_model_file, train_log_file]
+
+    # get evaluation files
+    evaluation_file = experiment_folder / "evaluation.json"
+    evaluation_meta_file = experiment_folder / "evaluation.meta.json"
+    evaluation_log_file = experiment_folder / "evaluation.pdf"
+    evaluation_files = [
+        evaluation_file,
+        evaluation_meta_file,
+        evaluation_log_file,
+    ]
+
+    # checks for maximum upload limit
+    total_size_mb = sum([size_in_mb(f) for f in train_files + evaluation_files])
+    if upload_limit_mb != 0 and total_size_mb > upload_limit_mb:
+        raise RuntimeError(
+            f"Total size of upload ({total_size_mb:.2f} MB) exceeds "
+            f"permitted maximum ({upload_limit_mb:.2f} MB)."
+        )
+
+    # prepare experiment and run names
+    with train_meta_file.open("r") as meta_file:
+        train_data = json.load(meta_file)
+
+    with evaluation_file.open("r") as meta_file:
+        evaluation_data = json.load(meta_file)
+    evaluation_data = evaluation_data["test"]
+
+    experiment_name = (
+        experiment_name
+        or f"{train_data['model-name']}-{train_data['database-name']}"
+    )
+    run_name = run_name or train_data["datetime"]
+
+    click.secho(
+        f"Uploading entry `{run_name}` to experiment `{experiment_name}` "
+        f"on GitLab project {project_path} (id: {project.id})...",
+        bold=True,
+        fg="green",
+    )
+    exp_meta = mlflow.set_experiment(experiment_name=experiment_name)
+    with mlflow.start_run(run_name=run_name):
+        click.echo("Uploading package metadata...")
+        click.echo(f"  -> `version` ({train_data['package-version']})")
+        mlflow.log_param("package version", train_data["package-version"])
+
+        click.echo("Uploading metrics...")
+
+        for k in [
+            "threshold",
+            "precision",
+            "recall",
+            "f1_score",
+            "average_precision_score",
+            "specificity",
+            "auc_score",
+            "accuracy",
+        ]:
+            click.secho(f"  -> `{k}` ({evaluation_data[k]:.3g})")
+            mlflow.log_metric(k, evaluation_data[k])
+
+        click.echo("Uploading artifacts (files)...")
+
+        with tempfile.TemporaryDirectory() as tmpdir_name:
+            tmpdir = pathlib.Path(tmpdir_name)
+            for f in train_files + evaluation_files:
+                assert f.exists(), f"File `{f}` does not exist - cannot upload!"
+                clean_path = str(sanitize_filename(tmpdir, f))
+                click.secho(f"  -> `{clean_path}` ({size_in_mb(f):.2f} MB)")
+                mlflow.log_artifact(clean_path)
+
+    click.secho(
+        f"Uploaded {total_size_mb:.2f} MB to server.", bold=True, fg="green"
+    )
+    click.secho(
+        f"Visit {gitlab.url}/{project.path_with_namespace}/-/ml/experiments/{exp_meta.experiment_id}",
+        bold=True,
+        fg="blue",
+    )
diff --git a/tests/conftest.py b/src/mednet/libs/common/tests/conftest.py
similarity index 100%
rename from tests/conftest.py
rename to src/mednet/libs/common/tests/conftest.py
diff --git a/tests/data/16bits.png b/src/mednet/libs/common/tests/data/16bits.png
similarity index 100%
rename from tests/data/16bits.png
rename to src/mednet/libs/common/tests/data/16bits.png
diff --git a/src/mednet/libs/common/tests/data/iris-test.csv b/src/mednet/libs/common/tests/data/iris-test.csv
new file mode 100644
index 0000000000000000000000000000000000000000..27d1b05a7aa70667844b74778504f3b51c624884
--- /dev/null
+++ b/src/mednet/libs/common/tests/data/iris-test.csv
@@ -0,0 +1,75 @@
+5,3,1.6,0.2,Iris-setosa
+5,3.4,1.6,0.4,Iris-setosa
+5.2,3.5,1.5,0.2,Iris-setosa
+5.2,3.4,1.4,0.2,Iris-setosa
+4.7,3.2,1.6,0.2,Iris-setosa
+4.8,3.1,1.6,0.2,Iris-setosa
+5.4,3.4,1.5,0.4,Iris-setosa
+5.2,4.1,1.5,0.1,Iris-setosa
+5.5,4.2,1.4,0.2,Iris-setosa
+4.9,3.1,1.5,0.1,Iris-setosa
+5,3.2,1.2,0.2,Iris-setosa
+5.5,3.5,1.3,0.2,Iris-setosa
+4.9,3.1,1.5,0.1,Iris-setosa
+4.4,3,1.3,0.2,Iris-setosa
+5.1,3.4,1.5,0.2,Iris-setosa
+5,3.5,1.3,0.3,Iris-setosa
+4.5,2.3,1.3,0.3,Iris-setosa
+4.4,3.2,1.3,0.2,Iris-setosa
+5,3.5,1.6,0.6,Iris-setosa
+5.1,3.8,1.9,0.4,Iris-setosa
+4.8,3,1.4,0.3,Iris-setosa
+5.1,3.8,1.6,0.2,Iris-setosa
+4.6,3.2,1.4,0.2,Iris-setosa
+5.3,3.7,1.5,0.2,Iris-setosa
+5,3.3,1.4,0.2,Iris-setosa
+6.6,3,4.4,1.4,Iris-versicolor
+6.8,2.8,4.8,1.4,Iris-versicolor
+6.7,3,5,1.7,Iris-versicolor
+6,2.9,4.5,1.5,Iris-versicolor
+5.7,2.6,3.5,1,Iris-versicolor
+5.5,2.4,3.8,1.1,Iris-versicolor
+5.5,2.4,3.7,1,Iris-versicolor
+5.8,2.7,3.9,1.2,Iris-versicolor
+6,2.7,5.1,1.6,Iris-versicolor
+5.4,3,4.5,1.5,Iris-versicolor
+6,3.4,4.5,1.6,Iris-versicolor
+6.7,3.1,4.7,1.5,Iris-versicolor
+6.3,2.3,4.4,1.3,Iris-versicolor
+5.6,3,4.1,1.3,Iris-versicolor
+5.5,2.5,4,1.3,Iris-versicolor
+5.5,2.6,4.4,1.2,Iris-versicolor
+6.1,3,4.6,1.4,Iris-versicolor
+5.8,2.6,4,1.2,Iris-versicolor
+5,2.3,3.3,1,Iris-versicolor
+5.6,2.7,4.2,1.3,Iris-versicolor
+5.7,3,4.2,1.2,Iris-versicolor
+5.7,2.9,4.2,1.3,Iris-versicolor
+6.2,2.9,4.3,1.3,Iris-versicolor
+5.1,2.5,3,1.1,Iris-versicolor
+5.7,2.8,4.1,1.3,Iris-versicolor
+7.2,3.2,6,1.8,Iris-virginica
+6.2,2.8,4.8,1.8,Iris-virginica
+6.1,3,4.9,1.8,Iris-virginica
+6.4,2.8,5.6,2.1,Iris-virginica
+7.2,3,5.8,1.6,Iris-virginica
+7.4,2.8,6.1,1.9,Iris-virginica
+7.9,3.8,6.4,2,Iris-virginica
+6.4,2.8,5.6,2.2,Iris-virginica
+6.3,2.8,5.1,1.5,Iris-virginica
+6.1,2.6,5.6,1.4,Iris-virginica
+7.7,3,6.1,2.3,Iris-virginica
+6.3,3.4,5.6,2.4,Iris-virginica
+6.4,3.1,5.5,1.8,Iris-virginica
+6,3,4.8,1.8,Iris-virginica
+6.9,3.1,5.4,2.1,Iris-virginica
+6.7,3.1,5.6,2.4,Iris-virginica
+6.9,3.1,5.1,2.3,Iris-virginica
+5.8,2.7,5.1,1.9,Iris-virginica
+6.8,3.2,5.9,2.3,Iris-virginica
+6.7,3.3,5.7,2.5,Iris-virginica
+6.7,3,5.2,2.3,Iris-virginica
+6.3,2.5,5,1.9,Iris-virginica
+6.5,3,5.2,2,Iris-virginica
+6.2,3.4,5.4,2.3,Iris-virginica
+5.9,3,5.1,1.8,Iris-virginica
diff --git a/src/mednet/libs/common/tests/data/iris-train.csv b/src/mednet/libs/common/tests/data/iris-train.csv
new file mode 100644
index 0000000000000000000000000000000000000000..82d5b134803975463f070aebe6847e7c742749d2
--- /dev/null
+++ b/src/mednet/libs/common/tests/data/iris-train.csv
@@ -0,0 +1,75 @@
+5.1,3.5,1.4,0.2,Iris-setosa
+4.9,3,1.4,0.2,Iris-setosa
+4.7,3.2,1.3,0.2,Iris-setosa
+4.6,3.1,1.5,0.2,Iris-setosa
+5,3.6,1.4,0.2,Iris-setosa
+5.4,3.9,1.7,0.4,Iris-setosa
+4.6,3.4,1.4,0.3,Iris-setosa
+5,3.4,1.5,0.2,Iris-setosa
+4.4,2.9,1.4,0.2,Iris-setosa
+4.9,3.1,1.5,0.1,Iris-setosa
+5.4,3.7,1.5,0.2,Iris-setosa
+4.8,3.4,1.6,0.2,Iris-setosa
+4.8,3,1.4,0.1,Iris-setosa
+4.3,3,1.1,0.1,Iris-setosa
+5.8,4,1.2,0.2,Iris-setosa
+5.7,4.4,1.5,0.4,Iris-setosa
+5.4,3.9,1.3,0.4,Iris-setosa
+5.1,3.5,1.4,0.3,Iris-setosa
+5.7,3.8,1.7,0.3,Iris-setosa
+5.1,3.8,1.5,0.3,Iris-setosa
+5.4,3.4,1.7,0.2,Iris-setosa
+5.1,3.7,1.5,0.4,Iris-setosa
+4.6,3.6,1,0.2,Iris-setosa
+5.1,3.3,1.7,0.5,Iris-setosa
+4.8,3.4,1.9,0.2,Iris-setosa
+7,3.2,4.7,1.4,Iris-versicolor
+6.4,3.2,4.5,1.5,Iris-versicolor
+6.9,3.1,4.9,1.5,Iris-versicolor
+5.5,2.3,4,1.3,Iris-versicolor
+6.5,2.8,4.6,1.5,Iris-versicolor
+5.7,2.8,4.5,1.3,Iris-versicolor
+6.3,3.3,4.7,1.6,Iris-versicolor
+4.9,2.4,3.3,1,Iris-versicolor
+6.6,2.9,4.6,1.3,Iris-versicolor
+5.2,2.7,3.9,1.4,Iris-versicolor
+5,2,3.5,1,Iris-versicolor
+5.9,3,4.2,1.5,Iris-versicolor
+6,2.2,4,1,Iris-versicolor
+6.1,2.9,4.7,1.4,Iris-versicolor
+5.6,2.9,3.6,1.3,Iris-versicolor
+6.7,3.1,4.4,1.4,Iris-versicolor
+5.6,3,4.5,1.5,Iris-versicolor
+5.8,2.7,4.1,1,Iris-versicolor
+6.2,2.2,4.5,1.5,Iris-versicolor
+5.6,2.5,3.9,1.1,Iris-versicolor
+5.9,3.2,4.8,1.8,Iris-versicolor
+6.1,2.8,4,1.3,Iris-versicolor
+6.3,2.5,4.9,1.5,Iris-versicolor
+6.1,2.8,4.7,1.2,Iris-versicolor
+6.4,2.9,4.3,1.3,Iris-versicolor
+6.3,3.3,6,2.5,Iris-virginica
+5.8,2.7,5.1,1.9,Iris-virginica
+7.1,3,5.9,2.1,Iris-virginica
+6.3,2.9,5.6,1.8,Iris-virginica
+6.5,3,5.8,2.2,Iris-virginica
+7.6,3,6.6,2.1,Iris-virginica
+4.9,2.5,4.5,1.7,Iris-virginica
+7.3,2.9,6.3,1.8,Iris-virginica
+6.7,2.5,5.8,1.8,Iris-virginica
+7.2,3.6,6.1,2.5,Iris-virginica
+6.5,3.2,5.1,2,Iris-virginica
+6.4,2.7,5.3,1.9,Iris-virginica
+6.8,3,5.5,2.1,Iris-virginica
+5.7,2.5,5,2,Iris-virginica
+5.8,2.8,5.1,2.4,Iris-virginica
+6.4,3.2,5.3,2.3,Iris-virginica
+6.5,3,5.5,1.8,Iris-virginica
+7.7,3.8,6.7,2.2,Iris-virginica
+7.7,2.6,6.9,2.3,Iris-virginica
+6,2.2,5,1.5,Iris-virginica
+6.9,3.2,5.7,2.3,Iris-virginica
+5.6,2.8,4.9,2,Iris-virginica
+7.7,2.8,6.7,2,Iris-virginica
+6.3,2.7,4.9,1.8,Iris-virginica
+6.7,3.3,5.7,2.1,Iris-virginica
diff --git a/tests/data/iris.json b/src/mednet/libs/common/tests/data/iris.json
similarity index 100%
rename from tests/data/iris.json
rename to src/mednet/libs/common/tests/data/iris.json
diff --git a/tests/data/raw_with_black_border.png b/src/mednet/libs/common/tests/data/raw_with_black_border.png
similarity index 100%
rename from tests/data/raw_with_black_border.png
rename to src/mednet/libs/common/tests/data/raw_with_black_border.png
diff --git a/tests/data/raw_with_elastic_deformation.png b/src/mednet/libs/common/tests/data/raw_with_elastic_deformation.png
similarity index 100%
rename from tests/data/raw_with_elastic_deformation.png
rename to src/mednet/libs/common/tests/data/raw_with_elastic_deformation.png
diff --git a/tests/data/raw_without_black_border.png b/src/mednet/libs/common/tests/data/raw_without_black_border.png
similarity index 100%
rename from tests/data/raw_without_black_border.png
rename to src/mednet/libs/common/tests/data/raw_without_black_border.png
diff --git a/tests/data/raw_without_elastic_deformation.png b/src/mednet/libs/common/tests/data/raw_without_elastic_deformation.png
similarity index 100%
rename from tests/data/raw_without_elastic_deformation.png
rename to src/mednet/libs/common/tests/data/raw_without_elastic_deformation.png
diff --git a/tests/test_database_split.py b/src/mednet/libs/common/tests/test_database_split.py
similarity index 100%
rename from tests/test_database_split.py
rename to src/mednet/libs/common/tests/test_database_split.py
diff --git a/tests/test_image_utils.py b/src/mednet/libs/common/tests/test_image_utils.py
similarity index 100%
rename from tests/test_image_utils.py
rename to src/mednet/libs/common/tests/test_image_utils.py
diff --git a/tests/test_resource_monitor.py b/src/mednet/libs/common/tests/test_resource_monitor.py
similarity index 100%
rename from tests/test_resource_monitor.py
rename to src/mednet/libs/common/tests/test_resource_monitor.py
diff --git a/tests/test_transforms.py b/src/mednet/libs/common/tests/test_transforms.py
similarity index 100%
rename from tests/test_transforms.py
rename to src/mednet/libs/common/tests/test_transforms.py
diff --git a/src/mednet/libs/common/utils/gitlab.py b/src/mednet/libs/common/utils/gitlab.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0b111ddcf1f54ec995ffb0544c0199aa3d4f7b4
--- /dev/null
+++ b/src/mednet/libs/common/utils/gitlab.py
@@ -0,0 +1,92 @@
+# SPDX-FileCopyrightText: Copyright © 2023 Idiap Research Institute <contact@idiap.ch>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import configparser
+import logging
+import pathlib
+import shutil
+
+import gitlab
+
+logger = logging.getLogger(__name__)
+
+
+def gitlab_instance_and_token() -> tuple[gitlab.Gitlab, str]:
+    """Return an instance of the Gitlab object for remote operations, and the
+    user token.
+
+    Returns
+    -------
+        Gitlab main object and user token
+    """
+
+    cfg = pathlib.Path("~/.python-gitlab.cfg").expanduser()
+    if cfg.exists():
+        gl = gitlab.Gitlab.from_config("idiap", [str(cfg)])
+        config = configparser.ConfigParser()
+        config.read(cfg)
+        token = config["idiap"]["private_token"]
+
+    else:  # ask the user for a token or use one from the current runner
+        server = "https://gitlab.idiap.ch"
+        token = input(f"{server} (user or project) token: ")
+        gl = gitlab.Gitlab(server, private_token=token, api_version="4")
+
+    # tests authentication with given credential.
+    gl.auth()
+
+    return gl, token
+
+
+def sanitize_filename(tmpdir: pathlib.Path, path: pathlib.Path) -> pathlib.Path:
+    """Sanitize the name of a file to be logged.
+
+    This function sanitizes the basename of a file to be logged on the GitLab
+    MLflow server. It removes unsupported characters (such as ``=``) by
+    creating a copy of the file to be uploaded, with a modified name, on the
+    provided temporary directory.  It then returns the name of such a temporary
+    file.
+
+    If the input file path does not need sanitization, it is returned as is.
+
+    Parameters
+    ----------
+    tmpdir
+        The temporary directory where a copy of the input path, with a
+        sanitized name will be created.
+    path
+        The file that needs its name sanitized.
+
+    Returns
+    -------
+        Path to the temporary folder, and the sanitized copy of the input file
+        in said temporary folder, or the input ``path``, in case its name does not
+        need sanitization.
+    """
+
+    sanitized_filename = path.parts[-1].replace("=", "-")
+    if path.parts[-1] == sanitized_filename:
+        return path
+
+    absolute_sanitized_filename = tmpdir / sanitized_filename
+    logger.info(
+        f"Sanitazing filename `{path}` -> `{absolute_sanitized_filename}`"
+    )
+    shutil.copy2(path, absolute_sanitized_filename)
+    return absolute_sanitized_filename
+
+
+def size_in_mb(path: pathlib.Path) -> float:
+    """Return the size in megabytes of a file.
+
+    Parameters
+    ----------
+    path
+        Input path to calculate file size from.
+
+    Returns
+    -------
+        A floating point number for the size of the object in MB.
+    """
+    return path.stat().st_size / (1024**2)
diff --git a/src/mednet/tests/test_cli.py b/src/mednet/tests/test_cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3a7d9a0065d649bfc0738bd03de3b0653da9e42
--- /dev/null
+++ b/src/mednet/tests/test_cli.py
@@ -0,0 +1,65 @@
+# SPDX-FileCopyrightText: Copyright © 2023 Idiap Research Institute <contact@idiap.ch>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+"""Tests for our CLI applications."""
+
+import contextlib
+
+from click.testing import CliRunner
+
+
+@contextlib.contextmanager
+def stdout_logging():
+    # copy logging messages to std out
+
+    import io
+    import logging
+
+    buf = io.StringIO()
+    ch = logging.StreamHandler(buf)
+    ch.setFormatter(logging.Formatter("%(message)s"))
+    ch.setLevel(logging.INFO)
+    logger = logging.getLogger("mednet")
+    logger.addHandler(ch)
+    yield buf
+    logger.removeHandler(ch)
+
+
+def _assert_exit_0(result):
+    assert (
+        result.exit_code == 0
+    ), f"Exit code {result.exit_code} != 0 -- Output:\n{result.output}"
+
+
+def _check_help(entry_point):
+    runner = CliRunner()
+    result = runner.invoke(entry_point, ["--help"])
+    _assert_exit_0(result)
+    assert result.output.startswith("Usage:")
+
+
+def test_info_help():
+    from mednet.scripts.info import info
+
+    _check_help(info)
+
+
+def test_info():
+    from mednet.scripts.info import info
+
+    runner = CliRunner()
+    result = runner.invoke(info)
+    _assert_exit_0(result)
+    assert "platform:" in result.output
+    assert "accelerators:" in result.output
+    assert "version:" in result.output
+    assert "configured classification databases:" in result.output
+    assert "configured segmentation databases:" in result.output
+    assert "dependencies:" in result.output
+    assert "python:" in result.output
+
+
+def test_upload_help():
+    from mednet.libs.common.scripts.upload import upload
+
+    _check_help(upload)