From cf2e6f902c8d5dd4011a41d01dc24a05fe440d2a Mon Sep 17 00:00:00 2001 From: Eero Aaltonen Date: Wed, 28 May 2025 15:58:09 +0300 Subject: [PATCH] Add support for deleting all builds of build name deleteAll parameter, added in Artifactory 2.3.0, allows to delete all build numbers of a specified build name. --- README.md | 7 +++++++ pyartifactory/models/build.py | 25 ++++++++++++++++++++++-- pyartifactory/objects/build.py | 18 +++++++++++------ tests/test_build.py | 35 +++++++++++++++++++++++++--------- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0bc21b78..0534111f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ This library enables you to manage Artifactory resources such as users, groups, + [Create build](#create-build) + [Promote a build](#promote-a-build) + [Delete one or more builds](#delete-one-or-more-builds) + + [Delete all build numbers of a build](#delete-all-build-numbers-of-a-build) + [Rename a build](#rename-a-build) + [Get differences between two builds](#get-differences-between-two-builds) * [Contributing](#contributing) @@ -553,6 +554,12 @@ build_delete_request = BuildDeleteRequest(buildName="", buildNumbers art.builds.delete(build_delete_request) ``` +#### Delete all build numbers of a build +```python +build_delete_request = BuildDeleteRequest(buildName="", deleteAll=True) +art.builds.delete(build_delete_request) +``` + #### Rename a build ```python art.builds.build_rename("", "") diff --git a/pyartifactory/models/build.py b/pyartifactory/models/build.py index e3025f04..67bd7736 100644 --- a/pyartifactory/models/build.py +++ b/pyartifactory/models/build.py @@ -5,7 +5,9 @@ from typing import Dict, List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_serializer, model_validator +from pydantic_core import PydanticCustomError +from typing_extensions import Self class BuildProperties(BaseModel): @@ -127,10 +129,29 @@ class BuildPromotionRequest(BaseModel): class BuildDeleteRequest(BaseModel): project: str = "" buildName: str - buildNumbers: List[str] + buildNumbers: List[str] = [] deleteArtifacts: bool = False deleteAll: bool = False + @model_validator(mode="after") + def check_numbers_or_delete_all(self) -> Self: + if self.buildNumbers or self.deleteAll: + return self + raise PydanticCustomError("request_schema_error", "Either buildNumbers or deleteAll must be specified", None) + + @model_serializer() + def serialize_model(self): + d = { + "project": self.project, + "buildName": self.buildName, + "buildNumbers": self.buildNumbers, + "deleteArtifacts": self.deleteArtifacts, + "deleteAll": self.deleteAll, + } + if self.deleteAll: + d.pop("buildNumbers") + return d + class BuildDiffResponseDetail(BaseModel): updated: List[str] = [] diff --git a/pyartifactory/objects/build.py b/pyartifactory/objects/build.py index 68661aca..55bacdbb 100644 --- a/pyartifactory/objects/build.py +++ b/pyartifactory/objects/build.py @@ -120,13 +120,19 @@ def delete(self, delete_build: BuildDeleteRequest) -> None: :return: None """ try: - for _build_number in delete_build.buildNumbers: - self._get( - f"api/{self._uri}/{delete_build.buildName}/{_build_number}", - ) - # all build numbers exist + if delete_build.buildNumbers: + for _build_number in delete_build.buildNumbers: + self._get( + f"api/{self._uri}/{delete_build.buildName}/{_build_number}", + ) + # all build numbers exist + self._post(f"api/{self._uri}/delete", json=delete_build.model_dump()) - logger.debug("Builds %s deleted from %s", ",".join(delete_build.buildNumbers), delete_build.buildName) + + if delete_build.buildNumbers: + logger.debug("Builds %s deleted from %s", ",".join(delete_build.buildNumbers), delete_build.buildName) + else: + logger.debug("Deleted all builds of %s", delete_build.buildName) except requests.exceptions.HTTPError as error: self._raise_exception(error) diff --git a/tests/test_build.py b/tests/test_build.py index f54744a8..e2c9e25b 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -30,7 +30,7 @@ BUILD_DIFF = BuildDiffResponse() BUILD_PROMOTION_REQUEST = BuildPromotionRequest(sourceRepo="repo-abc", targetRepo="repo-def") BUILD_PROMOTION_RESPONSE = BuildPromotionResult() -BUILD_DELETE_REQUEST = BuildDeleteRequest(buildName="build", buildNumbers=["abc", "123"]) +BUILD_DELETE_BUILDNUMBERS_REQUEST = BuildDeleteRequest(buildName="build", buildNumbers=["abc", "123"]) BUILD_DELETE_ERROR = BuildError(errors=[{"status": 404, "message": "Not found"}]) BUILD_CREATE_REQUEST = BuildCreateRequest(name="a-build", number="build-xx", started="2014-09-30T12:00:19.893+0300") @@ -170,33 +170,50 @@ def test_list_build(mocker): @responses.activate -def test_delete_build_success(mocker): - for _build_number in BUILD_DELETE_REQUEST.buildNumbers: +def test_delete_buildnumbers_success(mocker): + for _build_number in BUILD_DELETE_BUILDNUMBERS_REQUEST.buildNumbers: responses.add( responses.GET, - f"{URL}/api/build/{BUILD_DELETE_REQUEST.buildName}/{_build_number}", + f"{URL}/api/build/{BUILD_DELETE_BUILDNUMBERS_REQUEST.buildName}/{_build_number}", json=BUILD_INFO.model_dump(), status=200, ) - responses.add(responses.POST, f"{URL}/api/build/delete", json=BUILD_DELETE_REQUEST.model_dump(), status=200) + responses.add( + responses.POST, + f"{URL}/api/build/delete", + json=BUILD_DELETE_BUILDNUMBERS_REQUEST.model_dump(), + status=200, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "delete") + artifactory_build.delete(BUILD_DELETE_BUILDNUMBERS_REQUEST) + + +@responses.activate +def test_delete_all_success(mocker): + delete_all_request = BuildDeleteRequest(buildName="build", deleteAll=True) + assert "buildNumbers" not in delete_all_request.model_dump() + + responses.add(responses.POST, f"{URL}/api/build/delete", json=delete_all_request.model_dump(), status=200) artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) mocker.spy(artifactory_build, "delete") - artifactory_build.delete(BUILD_DELETE_REQUEST) + artifactory_build.delete(delete_all_request) @responses.activate def test_delete_build_error_not_exist(mocker): responses.add( responses.GET, - f"{URL}/api/build/{BUILD_DELETE_REQUEST.buildName}/{BUILD_DELETE_REQUEST.buildNumbers[0]}", + f"{URL}/api/build/{BUILD_DELETE_BUILDNUMBERS_REQUEST.buildName}/{BUILD_DELETE_BUILDNUMBERS_REQUEST.buildNumbers[0]}", json=BUILD_INFO.model_dump(), status=200, ) responses.add( responses.GET, - f"{URL}/api/build/{BUILD_DELETE_REQUEST.buildName}/{BUILD_DELETE_REQUEST.buildNumbers[-1]}", + f"{URL}/api/build/{BUILD_DELETE_BUILDNUMBERS_REQUEST.buildName}/{BUILD_DELETE_BUILDNUMBERS_REQUEST.buildNumbers[-1]}", body=NOT_FOUND_EXCEPTION_BODY, status=200, ) @@ -204,7 +221,7 @@ def test_delete_build_error_not_exist(mocker): artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) mocker.spy(artifactory_build, "delete") with pytest.raises(ArtifactoryError): - artifactory_build.delete(BUILD_DELETE_REQUEST) + artifactory_build.delete(BUILD_DELETE_BUILDNUMBERS_REQUEST) @responses.activate