diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b0ac9040..12d2ff4a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,39 @@ # CHANGELOG +## v5.5.0 (2025-01-28) + +### Chores + +- Add deprecation warning for mirror_pull functions + ([`7f6fd5c`](https://github.com/python-gitlab/python-gitlab/commit/7f6fd5c3aac5e2f18adf212adbce0ac04c7150e1)) + +- Relax typing constraints for response action + ([`f430078`](https://github.com/python-gitlab/python-gitlab/commit/f4300782485ee6c38578fa3481061bd621656b0e)) + +- **tests**: Catch deprecation warnings + ([`0c1af08`](https://github.com/python-gitlab/python-gitlab/commit/0c1af08bc73611d288f1f67248cff9c32c685808)) + +### Documentation + +- Add usage of pull mirror + ([`9b374b2`](https://github.com/python-gitlab/python-gitlab/commit/9b374b2c051f71b8ef10e22209b8e90730af9d9b)) + +- Remove old pull mirror implementation + ([`9e18672`](https://github.com/python-gitlab/python-gitlab/commit/9e186726c8a5ae70ca49c56b2be09b34dbf5b642)) + +### Features + +- **functional**: Add pull mirror test + ([`3b31ade`](https://github.com/python-gitlab/python-gitlab/commit/3b31ade152eb61363a68cf0509867ff8738ccdaf)) + +- **projects**: Add pull mirror class + ([`2411bff`](https://github.com/python-gitlab/python-gitlab/commit/2411bff4fd1dab6a1dd70070441b52e9a2927a63)) + +- **unit**: Add pull mirror tests + ([`5c11203`](https://github.com/python-gitlab/python-gitlab/commit/5c11203a8b281f6ab34f7e85073fadcfc395503c)) + + ## v5.4.0 (2025-01-28) ### Bug Fixes diff --git a/docs/api-objects.rst b/docs/api-objects.rst index c8d4b7891..d8e038ff5 100644 --- a/docs/api-objects.rst +++ b/docs/api-objects.rst @@ -52,6 +52,7 @@ API examples gl_objects/protected_container_repositories gl_objects/protected_environments gl_objects/protected_packages + gl_objects/pull_mirror gl_objects/releases gl_objects/runners gl_objects/remote_mirrors diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index 5697fd206..6e6c00ad4 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -246,14 +246,6 @@ Get a list of users for the repository:: # search for users users = p.users.list(search='pattern') -Start the pull mirroring process (EE edition):: - - project.mirror_pull() - -Get a project’s pull mirror details (EE edition):: - - mirror_pull_details = project.mirror_pull_details() - Import / Export =============== diff --git a/docs/gl_objects/pull_mirror.rst b/docs/gl_objects/pull_mirror.rst new file mode 100644 index 000000000..e62cd6a4e --- /dev/null +++ b/docs/gl_objects/pull_mirror.rst @@ -0,0 +1,38 @@ +###################### +Project Pull Mirror +###################### + +Pull Mirror allow you to set up pull mirroring for a project. + +References +========== + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectPullMirror` + + :class:`gitlab.v4.objects.ProjectPullMirrorManager` + + :attr:`gitlab.v4.objects.Project.pull_mirror` + +* GitLab API: https://docs.gitlab.com/ce/api/pull_mirror.html + +Examples +-------- + +Get the current pull mirror of a project:: + + mirrors = project.pull_mirror.get() + +Create (and enable) a remote mirror for a project:: + + mirror = project.pull_mirror.create({'url': 'https://gitlab.com/example.git', + 'enabled': True}) + +Update an existing remote mirror's attributes:: + + mirror.enabled = False + mirror.only_protected_branches = True + mirror.save() + +Start an sync of the pull mirror:: + + mirror.start() diff --git a/gitlab/_version.py b/gitlab/_version.py index f4415c059..94ca6cfdc 100644 --- a/gitlab/_version.py +++ b/gitlab/_version.py @@ -3,4 +3,4 @@ __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __title__ = "python-gitlab" -__version__ = "5.4.0" +__version__ = "5.5.0" diff --git a/gitlab/mixins.py b/gitlab/mixins.py index d2e1e0d5e..e738a5c0b 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -640,7 +640,7 @@ def download( def download( self, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -652,7 +652,7 @@ def download( def download( self, streamed: bool = False, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: bool = False, diff --git a/gitlab/utils.py b/gitlab/utils.py index b5ca73b09..d26518b3e 100644 --- a/gitlab/utils.py +++ b/gitlab/utils.py @@ -77,7 +77,7 @@ def format(self, record: logging.LogRecord) -> str: def response_content( response: requests.Response, streamed: bool, - action: Optional[Callable[[bytes], None]], + action: Optional[Callable[[bytes], Any]], chunk_size: int, *, iterator: bool, diff --git a/gitlab/v4/objects/artifacts.py b/gitlab/v4/objects/artifacts.py index ce6f90b99..99a231e0f 100644 --- a/gitlab/v4/objects/artifacts.py +++ b/gitlab/v4/objects/artifacts.py @@ -84,7 +84,7 @@ def download( ref_name: str, job: str, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -102,7 +102,7 @@ def download( ref_name: str, job: str, streamed: bool = False, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: bool = False, @@ -177,7 +177,7 @@ def raw( artifact_path: str, job: str, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -195,7 +195,7 @@ def raw( artifact_path: str, job: str, streamed: bool = False, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: bool = False, diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index ce2193c2c..e1f7b2290 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -308,7 +308,7 @@ def raw( file_path: str, ref: Optional[str] = None, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, diff --git a/gitlab/v4/objects/jobs.py b/gitlab/v4/objects/jobs.py index 0c77d76a7..b98255acc 100644 --- a/gitlab/v4/objects/jobs.py +++ b/gitlab/v4/objects/jobs.py @@ -152,7 +152,7 @@ def artifacts( def artifacts( self, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -229,7 +229,7 @@ def artifact( self, path: str, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -305,7 +305,7 @@ def trace( def trace( self, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, diff --git a/gitlab/v4/objects/packages.py b/gitlab/v4/objects/packages.py index c31809d80..24c1c6868 100644 --- a/gitlab/v4/objects/packages.py +++ b/gitlab/v4/objects/packages.py @@ -159,7 +159,7 @@ def download( package_version: str, file_name: str, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -177,7 +177,7 @@ def download( package_version: str, file_name: str, streamed: bool = False, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: bool = False, diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index b2e86a65d..60587fa13 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -128,6 +128,8 @@ "ProjectForkManager", "ProjectRemoteMirror", "ProjectRemoteMirrorManager", + "ProjectPullMirror", + "ProjectPullMirrorManager", "ProjectStorage", "ProjectStorageManager", "SharedProject", @@ -249,6 +251,7 @@ class Project( releases: ProjectReleaseManager resource_groups: ProjectResourceGroupManager remote_mirrors: "ProjectRemoteMirrorManager" + pull_mirror: "ProjectPullMirrorManager" repositories: ProjectRegistryRepositoryManager runners: ProjectRunnerManager secure_files: ProjectSecureFileManager @@ -520,7 +523,7 @@ def snapshot( self, wiki: bool = False, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -533,7 +536,7 @@ def snapshot( self, wiki: bool = False, streamed: bool = False, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: bool = False, @@ -605,6 +608,13 @@ def mirror_pull(self, **kwargs: Any) -> None: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server failed to perform the request """ + utils.warn( + message=( + "project.mirror_pull() is deprecated and will be removed in a " + "future major version. Use project.pull_mirror.start() instead." + ), + category=DeprecationWarning, + ) path = f"/projects/{self.encoded_id}/mirror/pull" self.manager.gitlab.http_post(path, **kwargs) @@ -625,6 +635,13 @@ def mirror_pull_details(self, **kwargs: Any) -> Dict[str, Any]: Returns: dict of the parsed json returned by the server """ + utils.warn( + message=( + "project.mirror_pull_details() is deprecated and will be removed in a " + "future major version. Use project.pull_mirror.get() instead." + ), + category=DeprecationWarning, + ) path = f"/projects/{self.encoded_id}/mirror/pull" result = self.manager.gitlab.http_get(path, **kwargs) if TYPE_CHECKING: @@ -1240,6 +1257,65 @@ class ProjectRemoteMirrorManager( _update_attrs = RequiredOptional(optional=("enabled", "only_protected_branches")) +class ProjectPullMirror(SaveMixin, RESTObject): + _id_attr = None + + +class ProjectPullMirrorManager(GetWithoutIdMixin, UpdateMixin, RESTManager): + _path = "/projects/{project_id}/mirror/pull" + _obj_cls = ProjectPullMirror + _from_parent_attrs = {"project_id": "id"} + _update_attrs = RequiredOptional(optional=("url",)) + + def get(self, **kwargs: Any) -> ProjectPullMirror: + return cast(ProjectPullMirror, super().get(**kwargs)) + + @exc.on_http_error(exc.GitlabCreateError) + def create(self, data: Dict[str, Any], **kwargs: Any) -> ProjectPullMirror: + """Create a new object. + + Args: + data: parameters to send to the server to create the + resource + **kwargs: Extra options to send to the server (e.g. sudo) + + Returns: + A new instance of the managed object class built with + the data sent by the server + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabCreateError: If the server cannot perform the request + """ + if TYPE_CHECKING: + assert data is not None + self._create_attrs.validate_attrs(data=data) + + if TYPE_CHECKING: + assert self.path is not None + server_data = self.gitlab.http_put(self.path, post_data=data, **kwargs) + + if TYPE_CHECKING: + assert not isinstance(server_data, requests.Response) + return self._obj_cls(self, server_data) + + @cli.register_custom_action(cls_names="ProjectPullMirrorManager") + @exc.on_http_error(exc.GitlabCreateError) + def start(self, **kwargs: Any) -> None: + """Start the pull mirroring process for the project. + + Args: + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabCreateError: If the server failed to perform the request + """ + if TYPE_CHECKING: + assert self.path is not None + self.gitlab.http_post(self.path, **kwargs) + + class ProjectStorage(RefreshMixin, RESTObject): pass diff --git a/gitlab/v4/objects/repositories.py b/gitlab/v4/objects/repositories.py index 85dba4b4d..aece75d74 100644 --- a/gitlab/v4/objects/repositories.py +++ b/gitlab/v4/objects/repositories.py @@ -146,7 +146,7 @@ def repository_raw_blob( self, sha: str, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -273,7 +273,7 @@ def repository_archive( self, sha: Optional[str] = None, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, diff --git a/gitlab/v4/objects/secure_files.py b/gitlab/v4/objects/secure_files.py index b329756d3..9a71a6302 100644 --- a/gitlab/v4/objects/secure_files.py +++ b/gitlab/v4/objects/secure_files.py @@ -54,7 +54,7 @@ def download( def download( self, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -66,7 +66,7 @@ def download( def download( self, streamed: bool = False, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: bool = False, diff --git a/gitlab/v4/objects/snippets.py b/gitlab/v4/objects/snippets.py index 46c618e33..ebb304a2d 100644 --- a/gitlab/v4/objects/snippets.py +++ b/gitlab/v4/objects/snippets.py @@ -61,7 +61,7 @@ def content( def content( self, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, @@ -237,7 +237,7 @@ def content( def content( self, streamed: Literal[True] = True, - action: Optional[Callable[[bytes], None]] = None, + action: Optional[Callable[[bytes], Any]] = None, chunk_size: int = 1024, *, iterator: Literal[False] = False, diff --git a/tests/functional/api/test_import_export.py b/tests/functional/api/test_import_export.py index e8d9c9abc..f7444c92c 100644 --- a/tests/functional/api/test_import_export.py +++ b/tests/functional/api/test_import_export.py @@ -50,7 +50,7 @@ def test_project_import_export(gl, project, temp_dir): raise Exception("Project export taking too much time") with open(temp_dir / "gitlab-export.tgz", "wb") as f: - export.download(streamed=True, action=f.write) # type: ignore[call-overload] + export.download(streamed=True, action=f.write) output = gl.projects.import_project( open(temp_dir / "gitlab-export.tgz", "rb"), diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py index 18c850680..edb7e31df 100644 --- a/tests/functional/api/test_projects.py +++ b/tests/functional/api/test_projects.py @@ -310,6 +310,24 @@ def test_project_remote_mirrors(project): mirror.delete() +def test_project_pull_mirrors(project): + mirror_url = "https://gitlab.example.com/root/mirror.git" + + mirror = project.pull_mirror.create({"url": mirror_url}) + assert mirror.url == mirror_url + + mirror.enabled = True + mirror.save() + + mirror = project.pull_mirror.get() + assert isinstance(mirror, gitlab.v4.objects.ProjectPullMirror) + assert mirror.url == mirror_url + assert mirror.enabled is True + + mirror.enabled = False + mirror.save() + + def test_project_services(project): # Use 'update' to create a service as we don't have a 'create' method and # to add one is somewhat complicated so it hasn't been done yet. diff --git a/tests/unit/objects/test_projects.py b/tests/unit/objects/test_projects.py index 84682dea3..65e19459c 100644 --- a/tests/unit/objects/test_projects.py +++ b/tests/unit/objects/test_projects.py @@ -768,11 +768,13 @@ def test_transfer_project(project, resp_transfer_project): def test_project_pull_mirror(project, resp_start_pull_mirroring_project): - project.mirror_pull() + with pytest.warns(DeprecationWarning, match="is deprecated"): + project.mirror_pull() def test_project_pull_mirror_details(project, resp_pull_mirror_details_project): - details = project.mirror_pull_details() + with pytest.warns(DeprecationWarning, match="is deprecated"): + details = project.mirror_pull_details() assert details["last_error"] is None assert details["update_status"] == "finished" diff --git a/tests/unit/objects/test_pull_mirror.py b/tests/unit/objects/test_pull_mirror.py new file mode 100644 index 000000000..3fa671bc2 --- /dev/null +++ b/tests/unit/objects/test_pull_mirror.py @@ -0,0 +1,67 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/pull_mirror.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ProjectPullMirror + + +@pytest.fixture +def resp_pull_mirror(): + content = { + "update_status": "none", + "url": "https://gitlab.example.com/root/mirror.git", + "last_error": None, + "last_update_at": "2024-12-03T08:01:05.466Z", + "last_update_started_at": "2024-12-03T08:01:05.342Z", + "last_successful_update_at": None, + "enabled": True, + "mirror_trigger_builds": False, + "only_mirror_protected_branches": None, + "mirror_overwrites_diverged_branches": None, + "mirror_branch_regex": None, + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/projects/1/mirror/pull", + json=content, + content_type="application/json", + status=200, + ) + + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/mirror/pull", + status=200, + ) + + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/mirror/pull", + json=content, + content_type="application/json", + status=200, + ) + + yield rsps + + +def test_create_project_pull_mirror(project, resp_pull_mirror): + mirror = project.pull_mirror.create( + {"url": "https://gitlab.example.com/root/mirror.git"} + ) + assert mirror.enabled + + +def test_start_project_pull_mirror(project, resp_pull_mirror): + project.pull_mirror.start() + + +def test_get_project_pull_mirror(project, resp_pull_mirror): + mirror = project.pull_mirror.get() + assert isinstance(mirror, ProjectPullMirror) + assert mirror.enabled