diff --git a/mypy.ini b/mypy.ini
index 66f1b26f..724c08ee 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,37 +1,40 @@
 [mypy]
 namespace_packages = True
 warn_unused_ignores = True
 
 
 # support for django magic: https://github.com/typeddjango/django-stubs
 plugins = mypy_django_plugin.main
 
 [mypy.plugins.django-stubs]
 django_settings_module = swh.deposit.settings.testing
 
 
 # 3rd party libraries without stubs (yet)
 
 [mypy-celery.*]
 ignore_missing_imports = True
 
 [mypy-iso8601.*]
 ignore_missing_imports = True
 
 [mypy-pkg_resources.*]
 ignore_missing_imports = True
 
 [mypy-psycopg2.*]
 ignore_missing_imports = True
 
 [mypy-pytest.*]
 ignore_missing_imports = True
 
+[tenacity.*]
+ignore_missing_imports = True
+
 [mypy-rest_framework.*]
 ignore_missing_imports = True
 
 [mypy-xmltodict.*]
 ignore_missing_imports = True
 
 [mypy-swh.loader.tar.*]
 ignore_missing_imports = True
diff --git a/setup.py b/setup.py
index 7926e240..8fea3aea 100755
--- a/setup.py
+++ b/setup.py
@@ -1,79 +1,79 @@
 #!/usr/bin/env python3
 # Copyright (C) 2015-2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from setuptools import setup, find_packages
 
 from os import path
 from io import open
 
 here = path.abspath(path.dirname(__file__))
 
 # Get the long description from the README file
 with open(path.join(here, "README.md"), encoding="utf-8") as f:
     long_description = f.read()
 
 
 def parse_requirements(*names):
     requirements = []
     for name in names:
         if name:
             reqf = "requirements-%s.txt" % name
         else:
             reqf = "requirements.txt"
 
         if not path.exists(reqf):
             return requirements
 
         with open(reqf) as f:
             for line in f.readlines():
                 line = line.strip()
                 if not line or line.startswith("#"):
                     continue
                 requirements.append(line)
     return requirements
 
 
 setup(
     name="swh.deposit",
     description="Software Heritage Deposit Server",
     long_description=long_description,
     long_description_content_type="text/markdown",
     python_requires=">=3.7",
     author="Software Heritage developers",
     author_email="swh-devel@inria.fr",
     url="https://forge.softwareheritage.org/source/swh-deposit/",
     packages=find_packages(),
     install_requires=parse_requirements(None, "swh"),
     tests_require=parse_requirements("test"),
-    setup_requires=["vcversioner"],
+    setup_requires=["setuptools-scm"],
+    use_scm_version=True,
     extras_require={
         "testing": parse_requirements("test", "server", "swh-server"),
         "server": parse_requirements("server", "swh-server"),
     },
-    vcversioner={},
     include_package_data=True,
     entry_points="""
         [console_scripts]
         swh-deposit=swh.deposit.cli:main
         [swh.cli.subcommands]
         deposit=swh.deposit.cli:deposit
         [swh.workers]
         deposit.worker=swh.deposit.loader:register
     """,
     classifiers=[
         "Programming Language :: Python :: 3",
         "Intended Audience :: Developers",
         "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
         "Operating System :: OS Independent",
         "Development Status :: 5 - Production/Stable",
     ],
     project_urls={
         "Bug Reports": "https://forge.softwareheritage.org/maniphest",
         "Funding": "https://www.softwareheritage.org/donate",
         "Source": "https://forge.softwareheritage.org/source/swh-deposit",
         "Documentation": "https://docs.softwareheritage.org/devel/swh-deposit/",
     },
 )
diff --git a/swh/deposit/tests/api/conftest.py b/swh/deposit/tests/api/conftest.py
index 1f5f779a..614f4400 100644
--- a/swh/deposit/tests/api/conftest.py
+++ b/swh/deposit/tests/api/conftest.py
@@ -1,87 +1,94 @@
-# Copyright (C) 2019  The Software Heritage developers
+# Copyright (C) 2019-2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import os
 import hashlib
 import pytest
 
 from django.urls import reverse
 
 from swh.deposit.config import (
     DEPOSIT_STATUS_DEPOSITED,
     COL_IRI,
     DEPOSIT_STATUS_VERIFIED,
 )
 from swh.deposit.models import Deposit
 from swh.deposit.parsers import parse_xml
 
 from swh.deposit.api.private.deposit_check import SWHChecksDeposit
 
 
+@pytest.fixture
+def datadir(request):
+    """Override default datadir to target main test datadir"""
+    return os.path.join(os.path.dirname(str(request.fspath)), "../data")
+
+
 @pytest.fixture
 def ready_deposit_ok(partial_deposit_with_metadata):
     """Returns a deposit ready for checks (it will pass the checks).
 
     """
     deposit = partial_deposit_with_metadata
     deposit.status = DEPOSIT_STATUS_DEPOSITED
     deposit.save()
     return deposit
 
 
 @pytest.fixture
 def ready_deposit_verified(partial_deposit_with_metadata):
     """Returns a deposit ready for checks (it will pass the checks).
 
     """
     deposit = partial_deposit_with_metadata
     deposit.status = DEPOSIT_STATUS_VERIFIED
     deposit.save()
     return deposit
 
 
 @pytest.fixture
 def ready_deposit_only_metadata(partial_deposit_only_metadata):
     """Deposit in status ready that will fail the checks (because missing
        archive).
 
     """
     deposit = partial_deposit_only_metadata
     deposit.status = DEPOSIT_STATUS_DEPOSITED
     deposit.save()
     return deposit
 
 
 @pytest.fixture
 def ready_deposit_invalid_archive(authenticated_client, deposit_collection):
     url = reverse(COL_IRI, args=[deposit_collection.name])
 
     data = b"some data which is clearly not a zip file"
     md5sum = hashlib.md5(data).hexdigest()
 
     # when
     response = authenticated_client.post(
         url,
         content_type="application/zip",  # as zip
         data=data,
         # + headers
         CONTENT_LENGTH=len(data),
         # other headers needs HTTP_ prefix to be taken into account
         HTTP_SLUG="external-id-invalid",
         HTTP_CONTENT_MD5=md5sum,
         HTTP_PACKAGING="http://purl.org/net/sword/package/SimpleZip",
         HTTP_CONTENT_DISPOSITION="attachment; filename=filename0",
     )
 
     response_content = parse_xml(response.content)
     deposit_id = int(response_content["deposit_id"])
     deposit = Deposit.objects.get(pk=deposit_id)
     deposit.status = DEPOSIT_STATUS_DEPOSITED
     deposit.save()
     return deposit
 
 
 @pytest.fixture
 def swh_checks_deposit():
     return SWHChecksDeposit()
diff --git a/swh/deposit/tests/api/data/atom b/swh/deposit/tests/api/data/atom
deleted file mode 120000
index 8be7c1c7..00000000
--- a/swh/deposit/tests/api/data/atom
+++ /dev/null
@@ -1 +0,0 @@
-../../data/atom
\ No newline at end of file
diff --git a/swh/deposit/tests/cli/data/atom b/swh/deposit/tests/cli/data/atom
deleted file mode 120000
index 8be7c1c7..00000000
--- a/swh/deposit/tests/cli/data/atom
+++ /dev/null
@@ -1 +0,0 @@
-../../data/atom
\ No newline at end of file
diff --git a/swh/deposit/tests/cli/test_client.py b/swh/deposit/tests/cli/test_client.py
index 7b5fd7ff..7a72cae9 100644
--- a/swh/deposit/tests/cli/test_client.py
+++ b/swh/deposit/tests/cli/test_client.py
@@ -1,457 +1,463 @@
 # Copyright (C) 2019-2020 The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import contextlib
 import logging
 import os
 import re
 from unittest.mock import MagicMock
 
 from click.testing import CliRunner
 import pytest
 
 from swh.deposit.client import PublicApiDepositClient, MaintenanceError
 from swh.deposit.cli.client import generate_slug, _url, _client, _collection, InputError
 from swh.deposit.cli import deposit as cli
 from ..conftest import TEST_USER
 
 
 EXAMPLE_SERVICE_DOCUMENT = {
     "service": {"workspace": {"collection": {"sword:name": "softcol",}}}
 }
 
 
+@pytest.fixture
+def datadir(request):
+    """Override default datadir to target main test datadir"""
+    return os.path.join(os.path.dirname(str(request.fspath)), "../data")
+
+
 @pytest.fixture
 def slug():
     return generate_slug()
 
 
 @pytest.fixture
 def client_mock(mocker, slug):
     """A successful deposit client with hard-coded default values
 
     """
     mocker.patch("swh.deposit.cli.client.generate_slug", return_value=slug)
     mock_client = MagicMock()
     mocker.patch("swh.deposit.cli.client._client", return_value=mock_client)
     mock_client.service_document.return_value = EXAMPLE_SERVICE_DOCUMENT
     mock_client.deposit_create.return_value = '{"foo": "bar"}'
     return mock_client
 
 
 @pytest.fixture
 def client_mock_api_down(mocker, slug):
     """A mock client whose connection with api fails due to maintenance issue
 
     """
     mocker.patch("swh.deposit.cli.client.generate_slug", return_value=slug)
     mock_client = MagicMock()
     mocker.patch("swh.deposit.cli.client._client", return_value=mock_client)
     mock_client.service_document.side_effect = MaintenanceError(
         "Database backend maintenance: Temporarily unavailable, try again later."
     )
     return mock_client
 
 
 def test_url():
     assert _url("http://deposit") == "http://deposit/1"
     assert _url("https://other/1") == "https://other/1"
 
 
 def test_client():
     client = _client("http://deposit", "user", "pass")
     assert isinstance(client, PublicApiDepositClient)
 
 
 def test_collection_error():
     mock_client = MagicMock()
     mock_client.service_document.return_value = {"error": "something went wrong"}
 
     with pytest.raises(InputError) as e:
         _collection(mock_client)
 
     assert "Service document retrieval: something went wrong" == str(e.value)
 
 
 def test_collection_ok():
     mock_client = MagicMock()
     mock_client.service_document.return_value = EXAMPLE_SERVICE_DOCUMENT
     collection_name = _collection(mock_client)
 
     assert collection_name == "softcol"
 
 
 def test_collection_ko_because_downtime():
     mock_client = MagicMock()
     mock_client.service_document.side_effect = MaintenanceError("downtime")
     with pytest.raises(MaintenanceError, match="downtime"):
         _collection(mock_client)
 
 
 def test_deposit_with_server_down_for_maintenance(
     sample_archive, mocker, caplog, client_mock_api_down, slug, tmp_path
 ):
     """ Deposit failure due to maintenance down time should be explicit
 
     """
     runner = CliRunner()
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--name",
             "test-project",
             "--archive",
             sample_archive["path"],
             "--author",
             "Jane Doe",
         ],
     )
 
     assert result.exit_code == 1, result.output
     assert result.output == ""
     assert caplog.record_tuples == [
         (
             "swh.deposit.cli.client",
             logging.ERROR,
             "Database backend maintenance: Temporarily unavailable, try again later.",
         )
     ]
 
     client_mock_api_down.service_document.assert_called_once_with()
 
 
 def test_single_minimal_deposit(
     sample_archive, mocker, caplog, client_mock, slug, tmp_path
 ):
     """ from:
     https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#single-deposit
     """  # noqa
 
     metadata_path = os.path.join(tmp_path, "metadata.xml")
     mocker.patch(
         "swh.deposit.cli.client.tempfile.TemporaryDirectory",
         return_value=contextlib.nullcontext(str(tmp_path)),
     )
 
     runner = CliRunner()
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--name",
             "test-project",
             "--archive",
             sample_archive["path"],
             "--author",
             "Jane Doe",
         ],
     )
 
     assert result.exit_code == 0, result.output
     assert result.output == ""
     assert caplog.record_tuples == [
         ("swh.deposit.cli.client", logging.INFO, '{"foo": "bar"}'),
     ]
 
     client_mock.deposit_create.assert_called_once_with(
         archive=sample_archive["path"],
         collection="softcol",
         in_progress=False,
         metadata=metadata_path,
         slug=slug,
     )
 
     with open(metadata_path) as fd:
         assert (
             fd.read()
             == f"""\
 <?xml version="1.0" encoding="utf-8"?>
 <entry xmlns="http://www.w3.org/2005/Atom" \
 xmlns:codemeta="https://doi.org/10.5063/SCHEMA/CODEMETA-2.0">
 \t<codemeta:name>test-project</codemeta:name>
 \t<codemeta:identifier>{slug}</codemeta:identifier>
 \t<codemeta:author>
 \t\t<codemeta:name>Jane Doe</codemeta:name>
 \t</codemeta:author>
 </entry>"""
         )
 
 
 def test_metadata_validation(sample_archive, mocker, caplog, tmp_path):
     """ from:
     https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#single-deposit
     """  # noqa
     slug = generate_slug()
     mocker.patch("swh.deposit.cli.client.generate_slug", return_value=slug)
     mock_client = MagicMock()
     mocker.patch("swh.deposit.cli.client._client", return_value=mock_client)
     mock_client.service_document.return_value = EXAMPLE_SERVICE_DOCUMENT
     mock_client.deposit_create.return_value = '{"foo": "bar"}'
 
     metadata_path = os.path.join(tmp_path, "metadata.xml")
     mocker.patch(
         "swh.deposit.cli.client.tempfile.TemporaryDirectory",
         return_value=contextlib.nullcontext(str(tmp_path)),
     )
     with open(metadata_path, "a"):
         pass  # creates the file
 
     runner = CliRunner()
 
     # Test missing author
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--name",
             "test-project",
             "--archive",
             sample_archive["path"],
         ],
     )
 
     assert result.exit_code == 1, result.output
     assert result.output == ""
     assert len(caplog.record_tuples) == 1
     (_logger, level, message) = caplog.record_tuples[0]
     assert level == logging.ERROR
     assert " --author " in message
 
     # Clear mocking state
     caplog.clear()
     mock_client.reset_mock()
 
     # Test missing name
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--archive",
             sample_archive["path"],
             "--author",
             "Jane Doe",
         ],
     )
 
     assert result.exit_code == 1, result.output
     assert result.output == ""
     assert len(caplog.record_tuples) == 1
     (_logger, level, message) = caplog.record_tuples[0]
     assert level == logging.ERROR
     assert " --name " in message
 
     # Clear mocking state
     caplog.clear()
     mock_client.reset_mock()
 
     # Test both --metadata and --author
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--archive",
             sample_archive["path"],
             "--metadata",
             metadata_path,
             "--author",
             "Jane Doe",
         ],
     )
 
     assert result.exit_code == 1, result.output
     assert result.output == ""
     assert len(caplog.record_tuples) == 1
     (_logger, level, message) = caplog.record_tuples[0]
     assert level == logging.ERROR
     assert re.search("--metadata.*is incompatible with", message)
 
     # Clear mocking state
     caplog.clear()
     mock_client.reset_mock()
 
 
 def test_single_deposit_slug_generation(
     sample_archive, mocker, caplog, tmp_path, client_mock
 ):
     """ from:
     https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#single-deposit
     """  # noqa
     slug = "my-slug"
     collection = "my-collection"
 
     metadata_path = os.path.join(tmp_path, "metadata.xml")
     mocker.patch(
         "swh.deposit.cli.client.tempfile.TemporaryDirectory",
         return_value=contextlib.nullcontext(str(tmp_path)),
     )
 
     runner = CliRunner()
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--name",
             "test-project",
             "--archive",
             sample_archive["path"],
             "--slug",
             slug,
             "--collection",
             collection,
             "--author",
             "Jane Doe",
         ],
     )
 
     assert result.exit_code == 0, result.output
     assert result.output == ""
     assert caplog.record_tuples == [
         ("swh.deposit.cli.client", logging.INFO, '{"foo": "bar"}'),
     ]
 
     client_mock.deposit_create.assert_called_once_with(
         archive=sample_archive["path"],
         collection=collection,
         in_progress=False,
         metadata=metadata_path,
         slug=slug,
     )
 
     with open(metadata_path) as fd:
         assert (
             fd.read()
             == """\
 <?xml version="1.0" encoding="utf-8"?>
 <entry xmlns="http://www.w3.org/2005/Atom" \
 xmlns:codemeta="https://doi.org/10.5063/SCHEMA/CODEMETA-2.0">
 \t<codemeta:name>test-project</codemeta:name>
 \t<codemeta:identifier>my-slug</codemeta:identifier>
 \t<codemeta:author>
 \t\t<codemeta:name>Jane Doe</codemeta:name>
 \t</codemeta:author>
 </entry>"""
         )
 
 
 def test_multisteps_deposit(
     sample_archive, atom_dataset, mocker, caplog, datadir, client_mock, slug
 ):
     """ from:
     https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#multisteps-deposit
     """  # noqa
     slug = generate_slug()
     mocker.patch("swh.deposit.cli.client.generate_slug", return_value=slug)
 
     # https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#create-an-incomplete-deposit
     client_mock.deposit_create.return_value = '{"deposit_id": "42"}'
 
     runner = CliRunner()
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--archive",
             sample_archive["path"],
             "--partial",
         ],
     )
 
     assert result.exit_code == 0, result.output
     assert result.output == ""
     assert caplog.record_tuples == [
         ("swh.deposit.cli.client", logging.INFO, '{"deposit_id": "42"}'),
     ]
 
     client_mock.deposit_create.assert_called_once_with(
         archive=sample_archive["path"],
         collection="softcol",
         in_progress=True,
         metadata=None,
         slug=slug,
     )
 
     # Clear mocking state
     caplog.clear()
     client_mock.reset_mock()
 
     # https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#add-content-or-metadata-to-the-deposit
 
     metadata_path = os.path.join(datadir, "atom", "entry-data-deposit-binary.xml")
 
     result = runner.invoke(
         cli,
         [
             "upload",
             "--url",
             "mock://deposit.swh/1",
             "--username",
             TEST_USER["username"],
             "--password",
             TEST_USER["password"],
             "--metadata",
             metadata_path,
         ],
     )
 
     assert result.exit_code == 0, result.output
     assert result.output == ""
     assert caplog.record_tuples == [
         ("swh.deposit.cli.client", logging.INFO, '{"deposit_id": "42"}'),
     ]
 
     client_mock.deposit_create.assert_called_once_with(
         archive=None,
         collection="softcol",
         in_progress=False,
         metadata=metadata_path,
         slug=slug,
     )
 
     # Clear mocking state
     caplog.clear()
     client_mock.reset_mock()