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"""\
\ttest-project
\t{slug}
\t
\t\tJane Doe
\t
"""
)
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()
== """\
\ttest-project
\tmy-slug
\t
\t\tJane Doe
\t
"""
)
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()