diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 001aca5e2..90f7d3fd5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,9 +22,9 @@ jobs: sphinx: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 - name: Set up Python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: "3.14" - name: Install dependencies @@ -37,9 +37,9 @@ jobs: twine-check: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 - name: Set up Python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: "3.14" - name: Install dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4148fcaeb..01bee8a33 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,10 +22,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 with: fetch-depth: 0 - - uses: actions/setup-python@v6.0.0 + - uses: actions/setup-python@v6.1.0 with: python-version: "3.14" - run: pip install --upgrade tox diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 05e21065c..f6f1c6229 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -15,6 +15,6 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5.0.1 + - uses: dessant/lock-threads@v6.0.0 with: process-only: 'issues' diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index 9bc366f6d..4be228d66 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -29,8 +29,8 @@ jobs: pre_commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v6.0.0 + - uses: actions/checkout@v6.0.1 + - uses: actions/setup-python@v6.1.0 with: python-version: "3.14" - name: install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a3cc1ec7..ff04716dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,14 +14,14 @@ jobs: id-token: write environment: pypi.org steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 with: fetch-depth: 0 token: ${{ secrets.RELEASE_GITHUB_TOKEN }} - name: Python Semantic Release id: release - uses: python-semantic-release/python-semantic-release@v10.4.1 + uses: python-semantic-release/python-semantic-release@v10.5.3 with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} @@ -32,7 +32,7 @@ jobs: if: steps.release.outputs.released == 'true' - name: Publish package distributions to GitHub Releases - uses: python-semantic-release/publish-action@v10.4.1 + uses: python-semantic-release/publish-action@v10.5.3 if: steps.release.outputs.released == 'true' with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e75866b21..e4fd014a5 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v10.1.0 + - uses: actions/stale@v10.1.1 with: operations-per-run: 500 stale-issue-label: "stale" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94dde7910..0f2af115e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,6 @@ jobs: matrix: os: [ubuntu-latest] python: - - version: "3.9" - toxenv: py39,smoke - version: "3.10" toxenv: py310,smoke - version: "3.11" @@ -36,21 +34,21 @@ jobs: toxenv: py312,smoke - version: "3.13" toxenv: py313,smoke - - version: "3.14.0-alpha - 3.14" # SemVer's version range syntax + - version: "3.14" toxenv: py314,smoke include: - os: macos-latest python: - version: "3.13" - toxenv: py313,smoke + version: "3.14" + toxenv: py314,smoke - os: windows-latest python: - version: "3.13" - toxenv: py313,smoke + version: "3.14" + toxenv: py314,smoke steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 - name: Set up Python ${{ matrix.python.version }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: ${{ matrix.python.version }} - name: Install dependencies @@ -67,9 +65,9 @@ jobs: matrix: toxenv: [api_func_v4, cli_func_v4] steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 - name: Set up Python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: "3.14" - name: Install dependencies @@ -79,7 +77,7 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox -- --override-ini='log_cli=True' - name: Upload codecov coverage - uses: codecov/codecov-action@v5.5.1 + uses: codecov/codecov-action@v5.5.2 with: files: ./coverage.xml flags: ${{ matrix.toxenv }} @@ -89,9 +87,9 @@ jobs: coverage: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: "3.14" - name: Install dependencies @@ -102,7 +100,7 @@ jobs: TOXENV: cover run: tox - name: Upload codecov coverage - uses: codecov/codecov-action@v5.5.1 + uses: codecov/codecov-action@v5.5.2 with: files: ./coverage.xml flags: unit @@ -113,8 +111,8 @@ jobs: runs-on: ubuntu-latest name: Python wheel steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v6.0.0 + - uses: actions/checkout@v6.0.1 + - uses: actions/setup-python@v6.1.0 with: python-version: "3.14" - name: Install dependencies @@ -122,7 +120,7 @@ jobs: pip install -r requirements-test.txt - name: Build package run: python -m build -o dist/ - - uses: actions/upload-artifact@v4.6.2 + - uses: actions/upload-artifact@v6.0.0 with: name: dist path: dist @@ -131,12 +129,12 @@ jobs: runs-on: ubuntu-latest needs: [dist] steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@v6.0.1 - name: Set up Python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@v6.1.0 with: python-version: '3.14' - - uses: actions/download-artifact@v5.0.0 + - uses: actions/download-artifact@v7.0.0 with: name: dist path: dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75b98ce66..79fd2c69c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,11 +3,11 @@ default_language_version: repos: - repo: https://github.com/psf/black - rev: 25.9.0 + rev: 25.12.0 hooks: - id: black - repo: https://github.com/commitizen-tools/commitizen - rev: v4.9.1 + rev: v4.11.1 hooks: - id: commitizen stages: [commit-msg] @@ -16,28 +16,28 @@ repos: hooks: - id: flake8 - repo: https://github.com/pycqa/isort - rev: 6.1.0 + rev: 7.0.0 hooks: - id: isort - repo: https://github.com/pycqa/pylint - rev: v3.3.9 + rev: v4.0.4 hooks: - id: pylint additional_dependencies: - argcomplete==2.0.0 - - gql==3.5.0 + - gql==4.0.0 - httpx==0.27.2 - pytest==7.4.2 - requests==2.28.1 - requests-toolbelt==1.0.0 files: 'gitlab/' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.1 hooks: - id: mypy args: [] additional_dependencies: - - gql==3.5.0 + - gql==4.0.0 - httpx==0.27.2 - jinja2==3.1.2 - pytest==7.4.2 @@ -51,6 +51,6 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/maxbrunet/pre-commit-renovate - rev: 41.146.0 + rev: 42.71.0 hooks: - id: renovate-config-validator diff --git a/.readthedocs.yml b/.readthedocs.yml index 2d561b88b..facdbd3f9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.13" sphinx: configuration: docs/conf.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f13242484..ef85c5019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ All versions below are listed in reverse chronological order. +## v7.1.0 (2025-12-28) + +### Bug Fixes + +- **utils**: Prevent negative sleep time in rate limit retry + ([`4221195`](https://github.com/python-gitlab/python-gitlab/commit/422119576287de30e1b70411c7ab0bbe39231af7)) + +### Continuous Integration + +- **release**: Use the correct token for publish to GitHub + ([`614a74c`](https://github.com/python-gitlab/python-gitlab/commit/614a74c00f027f70b8e48a6b2a2ddcd3f823bffa)) + +### Features + +- **registry-protection**: Add support for registry protection rule deletion + ([`9dd62c3`](https://github.com/python-gitlab/python-gitlab/commit/9dd62c3f5bcf3e082c2733bd4edc068f993c22ec)) + +### Testing + +- **functional**: Update to GitLab 18.6 and resolve issues found + ([`c7c139b`](https://github.com/python-gitlab/python-gitlab/commit/c7c139b9e7823ec1800a819233aee469355ee8d1)) + + +## v7.0.0 (2025-10-29) + +### Features + +- Drop Python 3.9 support and add Python 3.14 + ([`22941ac`](https://github.com/python-gitlab/python-gitlab/commit/22941acc3f331d5b683599c014ec962ece5d4b76)) + +### Breaking Changes + +- As of python-gitlab 7.0.0, Python 3.9 is no longer supported. Python 3.10 or higher is required. + + ## v6.5.0 (2025-10-17) ### Bug Fixes diff --git a/README.rst b/README.rst index 101add1eb..1b7b7ce48 100644 --- a/README.rst +++ b/README.rst @@ -53,7 +53,7 @@ Features Installation ------------ -As of 5.0.0, ``python-gitlab`` is compatible with Python 3.9+. +As of 7.0.0, ``python-gitlab`` is compatible with Python 3.10+. Use ``pip`` to install the latest stable version of ``python-gitlab``: diff --git a/gitlab/_version.py b/gitlab/_version.py index 5c642f8f0..8305f3a6c 100644 --- a/gitlab/_version.py +++ b/gitlab/_version.py @@ -3,4 +3,4 @@ __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __title__ = "python-gitlab" -__version__ = "6.5.0" +__version__ = "7.1.0" diff --git a/gitlab/client.py b/gitlab/client.py index 37dd4c2e6..a3cf1f31a 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -18,7 +18,6 @@ try: import gql import gql.transport.exceptions - import graphql import httpx from ._backends.graphql import GitlabAsyncTransport, GitlabTransport @@ -1350,7 +1349,7 @@ def __enter__(self) -> GraphQL: def __exit__(self, *args: Any) -> None: self._http_client.close() - def execute(self, request: str | graphql.Source, *args: Any, **kwargs: Any) -> Any: + def execute(self, request: str, *args: Any, **kwargs: Any) -> Any: parsed_document = self._gql(request) retry = utils.Retry( max_retries=self._max_retries, @@ -1420,9 +1419,7 @@ async def __aenter__(self) -> AsyncGraphQL: async def __aexit__(self, *args: Any) -> None: await self._http_client.aclose() - async def execute( - self, request: str | graphql.Source, *args: Any, **kwargs: Any - ) -> Any: + async def execute(self, request: str, *args: Any, **kwargs: Any) -> Any: parsed_document = self._gql(request) retry = utils.Retry( max_retries=self._max_retries, diff --git a/gitlab/utils.py b/gitlab/utils.py index bf37e09a5..cf1b5b7b0 100644 --- a/gitlab/utils.py +++ b/gitlab/utils.py @@ -133,7 +133,7 @@ def handle_retry_on_status( if "Retry-After" in headers: wait_time = int(headers["Retry-After"]) elif "RateLimit-Reset" in headers: - wait_time = int(headers["RateLimit-Reset"]) - time.time() + wait_time = max(0, int(headers["RateLimit-Reset"]) - time.time()) self.cur_retries += 1 time.sleep(wait_time) return True diff --git a/gitlab/v4/objects/registry_protection_repository_rules.py b/gitlab/v4/objects/registry_protection_repository_rules.py index 19d4bdf59..2a457a024 100644 --- a/gitlab/v4/objects/registry_protection_repository_rules.py +++ b/gitlab/v4/objects/registry_protection_repository_rules.py @@ -1,5 +1,12 @@ from gitlab.base import RESTObject -from gitlab.mixins import CreateMixin, ListMixin, SaveMixin, UpdateMethod, UpdateMixin +from gitlab.mixins import ( + CreateMixin, + DeleteMixin, + ListMixin, + SaveMixin, + UpdateMethod, + UpdateMixin, +) from gitlab.types import RequiredOptional __all__ = [ @@ -16,6 +23,7 @@ class ProjectRegistryRepositoryProtectionRuleManager( ListMixin[ProjectRegistryRepositoryProtectionRule], CreateMixin[ProjectRegistryRepositoryProtectionRule], UpdateMixin[ProjectRegistryRepositoryProtectionRule], + DeleteMixin[ProjectRegistryRepositoryProtectionRule], ): _path = "/projects/{project_id}/registry/protection/repository/rules" _obj_cls = ProjectRegistryRepositoryProtectionRule diff --git a/pyproject.toml b/pyproject.toml index 5aad28aee..45e8c36f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ maintainers = [ {name = "Nejc Habjan", email="nejc.habjan@siemens.com"}, {name = "Roger Meier", email="r.meier@siemens.com"} ] -requires-python = ">=3.9.0" +requires-python = ">=3.10.0" dependencies = [ "requests>=2.32.0", "requests-toolbelt>=1.0.0", @@ -30,11 +30,11 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] keywords = ["api", "client", "gitlab", "python", "python-gitlab", "wrapper"] license = {text = "LGPL-3.0-or-later"} @@ -43,7 +43,7 @@ dynamic = ["version"] [project.optional-dependencies] autocompletion = ["argcomplete>=1.10.0,<3"] yaml = ["PyYaml>=6.0.1"] -graphql = ["gql[httpx]>=3.5.0,<4"] +graphql = ["gql[httpx]>=3.5.0,<5"] [project.scripts] gitlab = "gitlab.cli:main" diff --git a/requirements-docker.txt b/requirements-docker.txt index 532609b3f..123a4438a 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,3 +1,3 @@ -r requirements.txt -r requirements-test.txt -pytest-docker==3.2.3 +pytest-docker==3.2.5 diff --git a/requirements-docs.txt b/requirements-docs.txt index b83ef6dac..8d756d4f1 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ -r requirements.txt -furo==2025.9.25 +furo==2025.12.19 jinja2==3.1.6 myst-parser==4.0.1 sphinx==8.2.3 diff --git a/requirements-lint.txt b/requirements-lint.txt index 81ca68a17..cf8092fb1 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,14 +1,14 @@ -r requirements.txt argcomplete==2.0.0 -black==25.9.0 -commitizen==4.9.1 +black==25.12.0 +commitizen==4.11.0 flake8==7.3.0 -isort==6.1.0 -mypy==1.18.2 -pylint==3.3.9 -pytest==8.4.2 +isort==7.0.0 +mypy==1.19.1 +pylint==4.0.4 +pytest==9.0.2 responses==0.25.8 respx==0.22.0 types-PyYAML==6.0.12.20250915 types-requests==2.32.4.20250913 -types-setuptools==80.9.0.20250822 +types-setuptools==80.9.0.20251223 diff --git a/requirements-precommit.txt b/requirements-precommit.txt index 3ee862221..fc2379223 100644 --- a/requirements-precommit.txt +++ b/requirements-precommit.txt @@ -1 +1 @@ -pre-commit==4.3.0 +pre-commit==4.5.1 diff --git a/requirements-test.txt b/requirements-test.txt index 369c95673..df8e6f47a 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,13 +1,13 @@ -r requirements.txt -anyio==4.11.0 +anyio==4.12.0 build==1.3.0 -coverage==7.10.7 +coverage==7.13.1 pytest-console-scripts==1.4.1 pytest-cov==7.0.0 pytest-github-actions-annotate-failures==0.3.0 -pytest==8.4.2 +pytest==9.0.2 PyYaml==6.0.3 responses==0.25.8 respx==0.22.0 -trio==0.31.0 +trio==0.32.0 wheel==0.45.1 diff --git a/requirements.txt b/requirements.txt index 6930e5d2c..d51e14533 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -gql==3.5.3 +gql==4.0.0 httpx==0.28.1 requests==2.32.5 requests-toolbelt==1.0.0 diff --git a/tests/functional/api/test_epics.py b/tests/functional/api/test_epics.py index a4f6765da..b61e23776 100644 --- a/tests/functional/api/test_epics.py +++ b/tests/functional/api/test_epics.py @@ -15,18 +15,20 @@ def test_epics(group): assert group.epics.list() -@pytest.mark.xfail(reason="404 on issue.id") def test_epic_issues(epic, issue): assert not epic.issues.list() + # FYI: Creating an issue causes a note to be created epic_issue = epic.issues.create({"issue_id": issue.id}) assert epic.issues.list() + # FYI: Deleting an issue causes a note to be created epic_issue.delete() def test_epic_notes(epic): - assert not epic.notes.list() + notes = epic.notes.list(get_all=True) epic.notes.create({"body": "Test note"}) - assert epic.notes.list() + new_notes = epic.notes.list(get_all=True) + assert len(new_notes) == (len(notes) + 1), f"{new_notes} {notes}" diff --git a/tests/functional/api/test_keys.py b/tests/functional/api/test_keys.py index 359649bef..6a2d660ed 100644 --- a/tests/functional/api/test_keys.py +++ b/tests/functional/api/test_keys.py @@ -38,6 +38,14 @@ def test_keys_deploy(gl, project, DEPLOY_KEY): key_by_fingerprint = gl.keys.get(fingerprint=fingerprint) assert key_by_fingerprint.title == key.title assert key_by_fingerprint.key == key.key - assert len(key_by_fingerprint.deploy_keys_projects) == 1 + + if not any( + key_project.get("project_id") == project.id + for key_project in key_by_fingerprint.deploy_keys_projects + ): + raise AssertionError( + f"Project {project} not found in 'deploy_keys_projects' " + f"{key_by_fingerprint.pformat()}" + ) key.delete() diff --git a/tests/functional/api/test_project_job_token_scope.py b/tests/functional/api/test_project_job_token_scope.py index 0d0466182..b1de0a7b2 100644 --- a/tests/functional/api/test_project_job_token_scope.py +++ b/tests/functional/api/test_project_job_token_scope.py @@ -1,3 +1,6 @@ +import pytest + + # https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-any-project-to-access-your-project def test_enable_limit_access_to_this_project(gl, project): scope = project.job_token_scope.get() @@ -10,6 +13,7 @@ def test_enable_limit_access_to_this_project(gl, project): assert scope.inbound_enabled +@pytest.mark.xfail(reason="https://gitlab.com/gitlab-org/gitlab/-/issues/582271") def test_disable_limit_access_to_this_project(gl, project): scope = project.job_token_scope.get() diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py index 760f95336..c56b23ec7 100644 --- a/tests/functional/api/test_projects.py +++ b/tests/functional/api/test_projects.py @@ -26,9 +26,9 @@ def test_create_project(gl, user): sudo_project = gl.projects.create({"name": "sudo_project"}, sudo=user.id) - created = gl.projects.list() + created = gl.projects.list(get_all=True) created_gen = gl.projects.list(iterator=True) - owned = gl.projects.list(owned=True) + owned = gl.projects.list(owned=True, get_all=True) assert admin_project in created and sudo_project in created assert admin_project in owned and sudo_project not in owned diff --git a/tests/functional/api/test_registry.py b/tests/functional/api/test_registry.py index 91fdceacc..d234128ca 100644 --- a/tests/functional/api/test_registry.py +++ b/tests/functional/api/test_registry.py @@ -26,3 +26,8 @@ def test_project_protected_registry(project: Project): protected_registry.minimum_access_level_for_push = "owner" protected_registry.save() assert protected_registry.minimum_access_level_for_push == "owner" + + protected_registry.delete() + + rules = project.registry_protection_repository_rules.list() + assert rules == [] diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env index e85f85e6f..91f187a12 100644 --- a/tests/functional/fixtures/.env +++ b/tests/functional/fixtures/.env @@ -1,4 +1,4 @@ GITLAB_IMAGE=gitlab/gitlab-ee -GITLAB_TAG=17.8.2-ee.0 +GITLAB_TAG=18.7.1-ee.0 GITLAB_RUNNER_IMAGE=gitlab/gitlab-runner GITLAB_RUNNER_TAG=96856197 diff --git a/tests/functional/fixtures/docker-compose.yml b/tests/functional/fixtures/docker-compose.yml index f36f3d2fd..17562d5be 100644 --- a/tests/functional/fixtures/docker-compose.yml +++ b/tests/functional/fixtures/docker-compose.yml @@ -34,7 +34,7 @@ services: entrypoint: - /bin/sh - -c - - ruby /create_license.rb && /assets/wrapper + - ruby /create_license.rb && /assets/init-container volumes: - ${PWD}/tests/functional/fixtures/create_license.rb:/create_license.rb ports: diff --git a/tests/functional/helpers.py b/tests/functional/helpers.py index 090673bf7..9d313e540 100644 --- a/tests/functional/helpers.py +++ b/tests/functional/helpers.py @@ -9,6 +9,7 @@ import gitlab import gitlab.base import gitlab.exceptions +import gitlab.v4.objects SLEEP_INTERVAL = 0.5 TIMEOUT = 60 # seconds before timeout will occur @@ -37,6 +38,11 @@ def safe_delete(object: gitlab.base.RESTObject) -> None: object = manager.get(object.get_id()) # type: ignore[attr-defined] except gitlab.exceptions.GitlabGetError: return + # If object is already marked for deletion we have succeeded + if getattr(object, "marked_for_deletion_on", None) is not None: + # 'Group' and 'Project' objects have a 'marked_for_deletion_on' attribute + logging.info(f"{object!r} is marked for deletion.") + return if index: logging.info(f"Attempt {index + 1} to delete {object!r}.") @@ -52,22 +58,16 @@ def safe_delete(object: gitlab.base.RESTObject) -> None: # we shouldn't cause test to fail if it still exists return elif isinstance(object, gitlab.v4.objects.Project): - # Immediately delete rather than waiting for at least 1day - # https://docs.gitlab.com/ee/api/projects.html#delete-project - object.delete(permanently_remove=True) - pass + # Starting in GitLab 18, projects can't be immediately deleted. + # So this will mark it for deletion. + object.delete() else: # We only attempt to delete parent groups to prevent dangling sub-groups - # However parent groups can only be deleted on a delay in Gl 16 + # However parent groups can only be deleted on a delay in GitLab 16 # https://docs.gitlab.com/ee/api/groups.html#remove-group object.delete() except gitlab.exceptions.GitlabDeleteError: - logging.info(f"{object!r} already deleted or scheduled for deletion.") - if isinstance(object, gitlab.v4.objects.Group): - # Parent groups can never be immediately deleted in GL 16, - # so don't cause test to fail if it still exists - return - pass + logging.exception(f"Error attempting to delete: {object.pformat()}") time.sleep(SLEEP_INTERVAL) pytest.fail(f"{object!r} was not deleted") diff --git a/tests/unit/objects/test_registry_protection_rules.py b/tests/unit/objects/test_registry_protection_rules.py index 3078278f5..3e9db414a 100644 --- a/tests/unit/objects/test_registry_protection_rules.py +++ b/tests/unit/objects/test_registry_protection_rules.py @@ -58,6 +58,17 @@ def resp_update_protected_registry(): yield rsps +@pytest.fixture +def resp_delete_protected_registry(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/projects/1/registry/protection/repository/rules/1", + status=204, + ) + yield rsps + + def test_list_project_protected_registries(project, resp_list_protected_registries): protected_registry = project.registry_protection_repository_rules.list()[0] assert isinstance(protected_registry, ProjectRegistryRepositoryProtectionRule) @@ -80,3 +91,7 @@ def test_update_project_protected_registry(project, resp_update_protected_regist 1, {"repository_path_pattern": "abc*"} ) assert updated["repository_path_pattern"] == "abc*" + + +def test_delete_project_protected_registry(project, resp_delete_protected_registry): + project.registry_protection_repository_rules.delete(1) diff --git a/tox.ini b/tox.ini index 05a15c6c4..0ba295692 100644 --- a/tox.ini +++ b/tox.ini @@ -2,14 +2,14 @@ minversion = 4.0 skipsdist = True skip_missing_interpreters = True -envlist = py313,py312,py311,py310,py39,black,isort,flake8,mypy,twine-check,cz,pylint +envlist = py314,py313,py312,py311,py310,black,isort,flake8,mypy,twine-check,cz,pylint # NOTE(jlvillal): To use a label use the `-m` flag. # For example to run the `func` label group of environments do: # tox -m func labels = lint = black,isort,flake8,mypy,pylint,cz - unit = py313,py312,py311,py310,py39,py38 + unit = py314,py313,py312,py311,py310 # func is the functional tests. This is very time consuming. func = cli_func_v4,api_func_v4