diff --git a/tests/test_upload.py b/tests/test_upload.py index e58ffd46..29438544 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -155,6 +155,21 @@ def test_skip_existing_skips_files_already_on_pypiserver(monkeypatch): package=pkg) is True +def test_skip_existing_skips_files_already_on_artifactory(monkeypatch): + # Artifactory (https://jfrog.com/artifactory/) responds with 403 + # when the file already exists. + response = pretend.stub( + status_code=403, + text="Not enough permissions to overwrite artifact " + "'pypi-local:twine/1.5.0/twine-1.5.0-py2.py3-none-any.whl'" + "(user 'twine-deployer' needs DELETE permission).") + + pkg = package.PackageFile.from_filename(WHEEL_FIXTURE, None) + assert upload.skip_upload(response=response, + skip_existing=True, + package=pkg) is True + + def test_skip_upload_respects_skip_existing(monkeypatch): response = pretend.stub( status_code=400, diff --git a/twine/commands/upload.py b/twine/commands/upload.py index 0689cb33..4fd2be50 100644 --- a/twine/commands/upload.py +++ b/twine/commands/upload.py @@ -57,18 +57,23 @@ def skip_upload(response, skip_existing, package): filename = package.basefilename # NOTE(sigmavirus24): Old PyPI returns the first message while Warehouse # returns the latter. This papers over the differences. - msg = ('A file named "{0}" already exists for'.format(filename), - 'File already exists') + msg_400 = ('A file named "{0}" already exists for'.format(filename), + 'File already exists') + msg_403 = 'Not enough permissions to overwrite artifact' # NOTE(sigmavirus24): PyPI presently returns a 400 status code with the # error message in the reason attribute. Other implementations return a - # 409 status code. We only want to skip an upload if: + # 409 or 403 status code. We only want to skip an upload if: # 1. The user has told us to skip existing packages (skip_existing is # True) AND # 2. a) The response status code is 409 OR # 2. b) The response status code is 400 AND it has a reason that matches - # what we expect PyPI to return to us. + # what we expect PyPI to return to us. OR + # 2. c) The response status code is 403 AND the text matches what we + # expect Artifactory to return to us. return (skip_existing and (response.status_code == 409 or - (response.status_code == 400 and response.reason.startswith(msg)))) + (response.status_code == 400 and + response.reason.startswith(msg_400)) or + (response.status_code == 403 and msg_403 in response.text))) def upload(dists, repository, sign, identity, username, password, comment,