From 5c11203a8b281f6ab34f7e85073fadcfc395503c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 23 Jan 2025 11:24:13 +0100 Subject: [PATCH 1/9] feat(unit): add pull mirror tests --- tests/unit/objects/test_pull_mirror.py | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/unit/objects/test_pull_mirror.py 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 From 2411bff4fd1dab6a1dd70070441b52e9a2927a63 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 23 Jan 2025 11:34:24 +0100 Subject: [PATCH 2/9] feat(projects): add pull mirror class --- gitlab/v4/objects/projects.py | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index b2e86a65d..09887eddd 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 @@ -1240,6 +1243,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 From 3b31ade152eb61363a68cf0509867ff8738ccdaf Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 23 Jan 2025 11:35:28 +0100 Subject: [PATCH 3/9] feat(functional): add pull mirror test --- tests/functional/api/test_projects.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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. From 9b374b2c051f71b8ef10e22209b8e90730af9d9b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 23 Jan 2025 11:36:34 +0100 Subject: [PATCH 4/9] docs: add usage of pull mirror --- docs/api-objects.rst | 1 + docs/gl_objects/pull_mirror.rst | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 docs/gl_objects/pull_mirror.rst 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/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() From 9e186726c8a5ae70ca49c56b2be09b34dbf5b642 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 23 Jan 2025 11:37:24 +0100 Subject: [PATCH 5/9] docs: remove old pull mirror implementation --- docs/gl_objects/projects.rst | 8 -------- 1 file changed, 8 deletions(-) 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 =============== From 7f6fd5c3aac5e2f18adf212adbce0ac04c7150e1 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 28 Jan 2025 09:20:53 +0100 Subject: [PATCH 6/9] chore: add deprecation warning for mirror_pull functions --- gitlab/v4/objects/projects.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index 09887eddd..e9990afeb 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -608,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) @@ -628,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: From f4300782485ee6c38578fa3481061bd621656b0e Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Tue, 28 Jan 2025 13:34:29 +0100 Subject: [PATCH 7/9] chore: relax typing constraints for response action --- gitlab/mixins.py | 4 ++-- gitlab/utils.py | 2 +- gitlab/v4/objects/artifacts.py | 8 ++++---- gitlab/v4/objects/files.py | 2 +- gitlab/v4/objects/jobs.py | 6 +++--- gitlab/v4/objects/packages.py | 4 ++-- gitlab/v4/objects/projects.py | 4 ++-- gitlab/v4/objects/repositories.py | 4 ++-- gitlab/v4/objects/secure_files.py | 4 ++-- gitlab/v4/objects/snippets.py | 4 ++-- tests/functional/api/test_import_export.py | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) 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 e9990afeb..60587fa13 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -523,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, @@ -536,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, 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"), From 0c1af08bc73611d288f1f67248cff9c32c685808 Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Tue, 28 Jan 2025 13:53:16 +0100 Subject: [PATCH 8/9] chore(tests): catch deprecation warnings --- tests/unit/objects/test_projects.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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" From cfa6358b4c9c44d267d8c98a53ac840fbb27d3e8 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 28 Jan 2025 14:00:38 +0000 Subject: [PATCH 9/9] chore: release v5.5.0 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ gitlab/_version.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) 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/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" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy