From 44fd9dc1176a2c5529c45cc3186c0e775026175e Mon Sep 17 00:00:00 2001 From: Igor Ponomarev Date: Mon, 30 Dec 2024 21:25:58 +0000 Subject: [PATCH 01/20] feat(api): Narrow down return type of download methods using typing.overload Currently the download methods such as `ProjectJob.artifacts` have return type set to `Optional[Union[bytes, Iterator[Any]]]` which means they return either `None` or `bytes` or `Iterator[Any]`. However, the actual return type is determined by the passed `streamed` and `iterator` arguments. Using `@typing.overload` decorator it is possible to return a single type based on the passed arguments. Add overloads in the following order to all download methods: 1. If `streamed=False` and `iterator=False` return `bytes`. This is the default argument values therefore it should be first as it will be used to lookup default arguments. 2. If `iterator=True` return `Iterator[Any]`. This can be combined with both `streamed=True` and `streamed=False`. 3. If `streamed=True` and `iterator=False` return `None`. In this case `action` argument can be set to a callable that accepts `bytes`. Signed-off-by: Igor Ponomarev --- gitlab/mixins.py | 35 +++++++ gitlab/v4/objects/artifacts.py | 92 ++++++++++++++++- gitlab/v4/objects/jobs.py | 115 ++++++++++++++++++++- gitlab/v4/objects/packages.py | 44 ++++++++ gitlab/v4/objects/projects.py | 38 +++++++ gitlab/v4/objects/repositories.py | 85 ++++++++++++++- gitlab/v4/objects/secure_files.py | 45 +++++++- gitlab/v4/objects/snippets.py | 79 +++++++++++++- tests/functional/api/test_import_export.py | 2 +- 9 files changed, 529 insertions(+), 6 deletions(-) diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 2a05278e0..d2e1e0d5e 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -6,7 +6,9 @@ Dict, Iterator, List, + Literal, Optional, + overload, Tuple, Type, TYPE_CHECKING, @@ -612,6 +614,39 @@ class DownloadMixin(_RestObjectBase): _updated_attrs: Dict[str, Any] manager: base.RESTManager + @overload + def download( + self, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def download( + self, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def download( + self, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names=("GroupExport", "ProjectExport")) @exc.on_http_error(exc.GitlabGetError) def download( diff --git a/gitlab/v4/objects/artifacts.py b/gitlab/v4/objects/artifacts.py index 4643ad3b1..ce6f90b99 100644 --- a/gitlab/v4/objects/artifacts.py +++ b/gitlab/v4/objects/artifacts.py @@ -3,7 +3,16 @@ https://docs.gitlab.com/ee/api/job_artifacts.html """ -from typing import Any, Callable, Iterator, Optional, TYPE_CHECKING, Union +from typing import ( + Any, + Callable, + Iterator, + Literal, + Optional, + overload, + TYPE_CHECKING, + Union, +) import requests @@ -43,6 +52,45 @@ def delete(self, **kwargs: Any) -> None: assert path is not None self.gitlab.http_delete(path, **kwargs) + @overload + def download( + self, + ref_name: str, + job: str, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def download( + self, + ref_name: str, + job: str, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def download( + self, + ref_name: str, + job: str, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action( cls_names="ProjectArtifactManager", required=("ref_name", "job"), @@ -94,6 +142,48 @@ def download( result, streamed, action, chunk_size, iterator=iterator ) + @overload + def raw( + self, + ref_name: str, + artifact_path: str, + job: str, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def raw( + self, + ref_name: str, + artifact_path: str, + job: str, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def raw( + self, + ref_name: str, + artifact_path: str, + job: str, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action( cls_names="ProjectArtifactManager", required=("ref_name", "artifact_path", "job"), diff --git a/gitlab/v4/objects/jobs.py b/gitlab/v4/objects/jobs.py index 28a46d775..0c77d76a7 100644 --- a/gitlab/v4/objects/jobs.py +++ b/gitlab/v4/objects/jobs.py @@ -1,4 +1,15 @@ -from typing import Any, Callable, cast, Dict, Iterator, Optional, TYPE_CHECKING, Union +from typing import ( + Any, + Callable, + cast, + Dict, + Iterator, + Literal, + Optional, + overload, + TYPE_CHECKING, + Union, +) import requests @@ -115,6 +126,39 @@ def delete_artifacts(self, **kwargs: Any) -> None: path = f"{self.manager.path}/{self.encoded_id}/artifacts" self.manager.gitlab.http_delete(path, **kwargs) + @overload + def artifacts( + self, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def artifacts( + self, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def artifacts( + self, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="ProjectJob") @exc.on_http_error(exc.GitlabGetError) def artifacts( @@ -156,6 +200,42 @@ def artifacts( result, streamed, action, chunk_size, iterator=iterator ) + @overload + def artifact( + self, + path: str, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def artifact( + self, + path: str, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def artifact( + self, + path: str, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="ProjectJob") @exc.on_http_error(exc.GitlabGetError) def artifact( @@ -199,6 +279,39 @@ def artifact( result, streamed, action, chunk_size, iterator=iterator ) + @overload + def trace( + self, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def trace( + self, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def trace( + self, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="ProjectJob") @exc.on_http_error(exc.GitlabGetError) def trace( diff --git a/gitlab/v4/objects/packages.py b/gitlab/v4/objects/packages.py index 8dcc3bdc4..c31809d80 100644 --- a/gitlab/v4/objects/packages.py +++ b/gitlab/v4/objects/packages.py @@ -11,7 +11,9 @@ Callable, cast, Iterator, + Literal, Optional, + overload, TYPE_CHECKING, Union, ) @@ -122,6 +124,48 @@ def upload( attrs.update(server_data) return self._obj_cls(self, attrs=attrs) + @overload + def download( + self, + package_name: str, + package_version: str, + file_name: str, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def download( + self, + package_name: str, + package_version: str, + file_name: str, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def download( + self, + package_name: str, + package_version: str, + file_name: str, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action( cls_names="GenericPackageManager", required=("package_name", "package_version", "file_name"), diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index 1fb36a01e..daf69c923 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -11,7 +11,9 @@ Dict, Iterator, List, + Literal, Optional, + overload, TYPE_CHECKING, Union, ) @@ -487,6 +489,42 @@ def restore(self, **kwargs: Any) -> None: path = f"/projects/{self.encoded_id}/restore" self.manager.gitlab.http_post(path, **kwargs) + @overload + def snapshot( + self, + wiki: bool = False, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def snapshot( + self, + wiki: bool = False, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def snapshot( + self, + wiki: bool = False, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="Project", optional=("wiki",)) @exc.on_http_error(exc.GitlabGetError) def snapshot( diff --git a/gitlab/v4/objects/repositories.py b/gitlab/v4/objects/repositories.py index 7d5b79df4..85dba4b4d 100644 --- a/gitlab/v4/objects/repositories.py +++ b/gitlab/v4/objects/repositories.py @@ -4,7 +4,18 @@ Currently this module only contains repository-related methods for projects. """ -from typing import Any, Callable, Dict, Iterator, List, Optional, TYPE_CHECKING, Union +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Literal, + Optional, + overload, + TYPE_CHECKING, + Union, +) import requests @@ -106,6 +117,42 @@ def repository_blob( path = f"/projects/{self.encoded_id}/repository/blobs/{sha}" return self.manager.gitlab.http_get(path, **kwargs) + @overload + def repository_raw_blob( + self, + sha: str, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def repository_raw_blob( + self, + sha: str, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def repository_raw_blob( + self, + sha: str, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="Project", required=("sha",)) @exc.on_http_error(exc.GitlabGetError) def repository_raw_blob( @@ -197,6 +244,42 @@ def repository_contributors( path = f"/projects/{self.encoded_id}/repository/contributors" return self.manager.gitlab.http_list(path, **kwargs) + @overload + def repository_archive( + self, + sha: Optional[str] = None, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def repository_archive( + self, + sha: Optional[str] = None, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def repository_archive( + self, + sha: Optional[str] = None, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="Project", optional=("sha", "format")) @exc.on_http_error(exc.GitlabListError) def repository_archive( diff --git a/gitlab/v4/objects/secure_files.py b/gitlab/v4/objects/secure_files.py index d96c129e4..b329756d3 100644 --- a/gitlab/v4/objects/secure_files.py +++ b/gitlab/v4/objects/secure_files.py @@ -3,7 +3,17 @@ https://docs.gitlab.com/ee/api/secure_files.html """ -from typing import Any, Callable, cast, Iterator, Optional, TYPE_CHECKING, Union +from typing import ( + Any, + Callable, + cast, + Iterator, + Literal, + Optional, + overload, + TYPE_CHECKING, + Union, +) import requests @@ -18,6 +28,39 @@ class ProjectSecureFile(ObjectDeleteMixin, RESTObject): + @overload + def download( + self, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def download( + self, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def download( + self, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="ProjectSecureFile") @exc.on_http_error(exc.GitlabGetError) def download( diff --git a/gitlab/v4/objects/snippets.py b/gitlab/v4/objects/snippets.py index 8d0fc06fc..a1453c103 100644 --- a/gitlab/v4/objects/snippets.py +++ b/gitlab/v4/objects/snippets.py @@ -1,4 +1,15 @@ -from typing import Any, Callable, cast, Iterator, List, Optional, TYPE_CHECKING, Union +from typing import ( + Any, + Callable, + cast, + Iterator, + List, + Literal, + Optional, + overload, + TYPE_CHECKING, + Union, +) import requests @@ -24,6 +35,39 @@ class Snippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _repr_attr = "title" + @overload + def content( + self, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def content( + self, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def content( + self, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="Snippet") @exc.on_http_error(exc.GitlabGetError) def content( @@ -167,6 +211,39 @@ class ProjectSnippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObj discussions: ProjectSnippetDiscussionManager notes: ProjectSnippetNoteManager + @overload + def content( + self, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def content( + self, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def content( + self, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action(cls_names="ProjectSnippet") @exc.on_http_error(exc.GitlabGetError) def content( diff --git a/tests/functional/api/test_import_export.py b/tests/functional/api/test_import_export.py index 6f70a810a..e8d9c9abc 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[arg-type] + export.download(streamed=True, action=f.write) # type: ignore[call-overload] output = gl.projects.import_project( open(temp_dir / "gitlab-export.tgz", "rb"), From 1e95944119455875bd239752cdf0fe5cc27707ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 01:27:19 +0000 Subject: [PATCH 02/20] chore(deps): update gitlab/gitlab-ee docker tag to v17.7.1-ee.0 (#3082) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tests/functional/fixtures/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env index d8b0d2a33..f333de1a1 100644 --- a/tests/functional/fixtures/.env +++ b/tests/functional/fixtures/.env @@ -1,2 +1,2 @@ GITLAB_IMAGE=gitlab/gitlab-ee -GITLAB_TAG=17.7.0-ee.0 +GITLAB_TAG=17.7.1-ee.0 From e3cb806dc368af0a495087531ee94892d3f240ce Mon Sep 17 00:00:00 2001 From: Igor Ponomarev Date: Wed, 15 Jan 2025 13:30:22 +0000 Subject: [PATCH 03/20] fix(api): Make type ignores more specific where possible Instead of using absolute ignore `# type: ignore` use a more specific ignores like `# type: ignore[override]`. This might help in the future where a new bug might be introduced and get ignored by a general ignore comment but not a more specific one. Signed-off-by: Igor Ponomarev --- gitlab/v4/objects/files.py | 8 ++++---- gitlab/v4/objects/issues.py | 2 +- gitlab/v4/objects/labels.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index b880bc9dd..e6ee27ba2 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -51,7 +51,7 @@ def decode(self) -> bytes: # NOTE(jlvillal): Signature doesn't match SaveMixin.save() so ignore # type error - def save( # type: ignore + def save( # type: ignore[override] self, branch: str, commit_message: str, **kwargs: Any ) -> None: """Save the changes made to the file to the server. @@ -75,7 +75,7 @@ def save( # type: ignore @exc.on_http_error(exc.GitlabDeleteError) # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore # type error - def delete( # type: ignore + def delete( # type: ignore[override] self, branch: str, commit_message: str, **kwargs: Any ) -> None: """Delete the file from the server. @@ -219,7 +219,7 @@ def create( @exc.on_http_error(exc.GitlabUpdateError) # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore # type error - def update( # type: ignore + def update( # type: ignore[override] self, file_path: str, new_data: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> Dict[str, Any]: """Update an object on the server. @@ -254,7 +254,7 @@ def update( # type: ignore @exc.on_http_error(exc.GitlabDeleteError) # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore # type error - def delete( # type: ignore + def delete( # type: ignore[override] self, file_path: str, branch: str, commit_message: str, **kwargs: Any ) -> None: """Delete a file on the server. diff --git a/gitlab/v4/objects/issues.py b/gitlab/v4/objects/issues.py index 867deec03..df6cf7a5a 100644 --- a/gitlab/v4/objects/issues.py +++ b/gitlab/v4/objects/issues.py @@ -302,7 +302,7 @@ class ProjectIssueLinkManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): @exc.on_http_error(exc.GitlabCreateError) # NOTE(jlvillal): Signature doesn't match CreateMixin.create() so ignore # type error - def create( # type: ignore + def create( # type: ignore[override] self, data: Dict[str, Any], **kwargs: Any ) -> Tuple[RESTObject, RESTObject]: """Create a new object. diff --git a/gitlab/v4/objects/labels.py b/gitlab/v4/objects/labels.py index 32d4f6ba0..b23062ec9 100644 --- a/gitlab/v4/objects/labels.py +++ b/gitlab/v4/objects/labels.py @@ -66,7 +66,7 @@ def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GroupLa # Update without ID. # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore # type error - def update( # type: ignore + def update( # type: ignore[override] self, name: Optional[str], new_data: Optional[Dict[str, Any]] = None, @@ -132,7 +132,7 @@ def get( # Update without ID. # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore # type error - def update( # type: ignore + def update( # type: ignore[override] self, name: Optional[str], new_data: Optional[Dict[str, Any]] = None, From 671e711c341d28ae0bc61ccb12d2e986353473fd Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Sun, 22 Dec 2024 21:01:21 -0800 Subject: [PATCH 04/20] chore(deps): update mypy to 1.14 and resolve issues mypy 1.14 has a change to Enum Membership Semantics: https://mypy.readthedocs.io/en/latest/changelog.html Resolve the issues with Enum and typing, and update mypy to 1.14 --- .pre-commit-config.yaml | 2 +- gitlab/const.py | 104 ++++++++++++++++++++-------------------- requirements-lint.txt | 2 +- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 285d2cf2a..01605556e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - requests-toolbelt==1.0.0 files: 'gitlab/' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.0 hooks: - id: mypy args: [] diff --git a/gitlab/const.py b/gitlab/const.py index b01ebd3c9..9e0b766ea 100644 --- a/gitlab/const.py +++ b/gitlab/const.py @@ -9,83 +9,83 @@ class GitlabEnum(str, Enum): # https://gitlab.com/gitlab-org/gitlab/-/blob/e97357824bedf007e75f8782259fe07435b64fbb/lib/gitlab/access.rb#L12-18 class AccessLevel(IntEnum): - NO_ACCESS: int = 0 - MINIMAL_ACCESS: int = 5 - GUEST: int = 10 - PLANNER: int = 15 - REPORTER: int = 20 - DEVELOPER: int = 30 - MAINTAINER: int = 40 - OWNER: int = 50 - ADMIN: int = 60 + NO_ACCESS = 0 + MINIMAL_ACCESS = 5 + GUEST = 10 + PLANNER = 15 + REPORTER = 20 + DEVELOPER = 30 + MAINTAINER = 40 + OWNER = 50 + ADMIN = 60 # https://gitlab.com/gitlab-org/gitlab/-/blob/e97357824bedf007e75f8782259fe07435b64fbb/lib/gitlab/visibility_level.rb#L23-25 class Visibility(GitlabEnum): - PRIVATE: str = "private" - INTERNAL: str = "internal" - PUBLIC: str = "public" + PRIVATE = "private" + INTERNAL = "internal" + PUBLIC = "public" class NotificationLevel(GitlabEnum): - DISABLED: str = "disabled" - PARTICIPATING: str = "participating" - WATCH: str = "watch" - GLOBAL: str = "global" - MENTION: str = "mention" - CUSTOM: str = "custom" + DISABLED = "disabled" + PARTICIPATING = "participating" + WATCH = "watch" + GLOBAL = "global" + MENTION = "mention" + CUSTOM = "custom" # https://gitlab.com/gitlab-org/gitlab/-/blob/e97357824bedf007e75f8782259fe07435b64fbb/app/views/search/_category.html.haml#L10-37 class SearchScope(GitlabEnum): # all scopes (global, group and project) - PROJECTS: str = "projects" - ISSUES: str = "issues" - MERGE_REQUESTS: str = "merge_requests" - MILESTONES: str = "milestones" - WIKI_BLOBS: str = "wiki_blobs" - COMMITS: str = "commits" - BLOBS: str = "blobs" - USERS: str = "users" + PROJECTS = "projects" + ISSUES = "issues" + MERGE_REQUESTS = "merge_requests" + MILESTONES = "milestones" + WIKI_BLOBS = "wiki_blobs" + COMMITS = "commits" + BLOBS = "blobs" + USERS = "users" # specific global scope - GLOBAL_SNIPPET_TITLES: str = "snippet_titles" + GLOBAL_SNIPPET_TITLES = "snippet_titles" # specific project scope - PROJECT_NOTES: str = "notes" + PROJECT_NOTES = "notes" # https://docs.gitlab.com/ee/api/merge_requests.html#merge-status class DetailedMergeStatus(GitlabEnum): # possible values for the detailed_merge_status field of Merge Requests - BLOCKED_STATUS: str = "blocked_status" - BROKEN_STATUS: str = "broken_status" - CHECKING: str = "checking" - UNCHECKED: str = "unchecked" - CI_MUST_PASS: str = "ci_must_pass" - CI_STILL_RUNNING: str = "ci_still_running" - DISCUSSIONS_NOT_RESOLVED: str = "discussions_not_resolved" - DRAFT_STATUS: str = "draft_status" - EXTERNAL_STATUS_CHECKS: str = "external_status_checks" - MERGEABLE: str = "mergeable" - NOT_APPROVED: str = "not_approved" - NOT_OPEN: str = "not_open" - POLICIES_DENIED: str = "policies_denied" + BLOCKED_STATUS = "blocked_status" + BROKEN_STATUS = "broken_status" + CHECKING = "checking" + UNCHECKED = "unchecked" + CI_MUST_PASS = "ci_must_pass" + CI_STILL_RUNNING = "ci_still_running" + DISCUSSIONS_NOT_RESOLVED = "discussions_not_resolved" + DRAFT_STATUS = "draft_status" + EXTERNAL_STATUS_CHECKS = "external_status_checks" + MERGEABLE = "mergeable" + NOT_APPROVED = "not_approved" + NOT_OPEN = "not_open" + POLICIES_DENIED = "policies_denied" # https://docs.gitlab.com/ee/api/pipelines.html class PipelineStatus(GitlabEnum): - CREATED: str = "created" - WAITING_FOR_RESOURCE: str = "waiting_for_resource" - PREPARING: str = "preparing" - PENDING: str = "pending" - RUNNING: str = "running" - SUCCESS: str = "success" - FAILED: str = "failed" - CANCELED: str = "canceled" - SKIPPED: str = "skipped" - MANUAL: str = "manual" - SCHEDULED: str = "scheduled" + CREATED = "created" + WAITING_FOR_RESOURCE = "waiting_for_resource" + PREPARING = "preparing" + PENDING = "pending" + RUNNING = "running" + SUCCESS = "success" + FAILED = "failed" + CANCELED = "canceled" + SKIPPED = "skipped" + MANUAL = "manual" + SCHEDULED = "scheduled" DEFAULT_URL: str = "https://gitlab.com" diff --git a/requirements-lint.txt b/requirements-lint.txt index c220740db..9b6b62c17 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -4,7 +4,7 @@ black==24.10.0 commitizen==4.1.0 flake8==7.1.1 isort==5.13.2 -mypy==1.13.0 +mypy==1.14.0 pylint==3.3.2 pytest==8.3.4 responses==0.25.3 From 2dda9dc149668a99211daaa1981bb1f422c63880 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Tue, 21 Jan 2025 16:06:52 -0800 Subject: [PATCH 05/20] ci: use gitlab-runner:v17.7.1 for the CI The `latest` gitlab-runner image does not have the `gitlab-runner` user and it causes our tests to fail. Closes: #3091 --- .renovaterc.json | 11 +++++++++++ tests/functional/fixtures/.env | 2 ++ tests/functional/fixtures/docker-compose.yml | 2 +- tox.ini | 2 ++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.renovaterc.json b/.renovaterc.json index ea63c6cef..29fffb8f5 100644 --- a/.renovaterc.json +++ b/.renovaterc.json @@ -23,6 +23,17 @@ "depNameTemplate": "gitlab/gitlab-ee", "datasourceTemplate": "docker", "versioningTemplate": "loose" + }, + { + "fileMatch": [ + "(^|/)tests\\/functional\\/fixtures\\/\\.env$" + ], + "matchStrings": [ + "GITLAB_RUNNER_TAG=(?.*?)\n" + ], + "depNameTemplate": "gitlab/gitlab-runner", + "datasourceTemplate": "docker", + "versioningTemplate": "loose" } ], "packageRules": [ diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env index f333de1a1..b687e0add 100644 --- a/tests/functional/fixtures/.env +++ b/tests/functional/fixtures/.env @@ -1,2 +1,4 @@ GITLAB_IMAGE=gitlab/gitlab-ee GITLAB_TAG=17.7.1-ee.0 +GITLAB_RUNNER_IMAGE=gitlab/gitlab-runner +GITLAB_RUNNER_TAG=v17.7.1 diff --git a/tests/functional/fixtures/docker-compose.yml b/tests/functional/fixtures/docker-compose.yml index e79fdf06a..550ec156c 100644 --- a/tests/functional/fixtures/docker-compose.yml +++ b/tests/functional/fixtures/docker-compose.yml @@ -45,7 +45,7 @@ services: - gitlab-network gitlab-runner: - image: gitlab/gitlab-runner:latest + image: '${GITLAB_RUNNER_IMAGE}:${GITLAB_RUNNER_TAG}' container_name: 'gitlab-runner-test' depends_on: - gitlab diff --git a/tox.ini b/tox.ini index dd691879f..c8b0f71ec 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,8 @@ passenv = GITHUB_WORKSPACE GITLAB_IMAGE GITLAB_TAG + GITLAB_RUNNER_IMAGE + GITLAB_RUNNER_TAG NO_COLOR PWD PY_COLORS From e4673d8aeaf97b9ad5d2500e459526b4cf494547 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 22 Jan 2025 08:29:54 -0800 Subject: [PATCH 06/20] chore(test): prevent 'job_with_artifact' fixture running forever Previously the 'job_with_artifact' fixture could run forever. Now give it up to 60 seconds to complete before failing. --- tests/functional/cli/test_cli_artifacts.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/functional/cli/test_cli_artifacts.py b/tests/functional/cli/test_cli_artifacts.py index f0e6f213f..589486844 100644 --- a/tests/functional/cli/test_cli_artifacts.py +++ b/tests/functional/cli/test_cli_artifacts.py @@ -1,3 +1,4 @@ +import logging import subprocess import textwrap import time @@ -24,12 +25,22 @@ @pytest.fixture(scope="module") def job_with_artifacts(gitlab_runner, project): + start_time = time.time() + project.files.create(data) jobs = None while not jobs: time.sleep(0.5) jobs = project.jobs.list(scope="success") + if time.time() - start_time < 60: + continue + logging.error("job never succeeded") + for job in project.jobs.list(): + job = project.jobs.get(job.id) + logging.info(f"{job.status} job: {job.pformat()}") + logging.info(f"job log:\n{job.trace()}\n") + pytest.fail("Fixture 'job_with_artifact' failed") return project.jobs.get(jobs[0].id) From fb07b5cfe1d986c3a7cd7879b11ecc43c75542b7 Mon Sep 17 00:00:00 2001 From: Igor Ponomarev Date: Thu, 16 Jan 2025 11:46:57 +0000 Subject: [PATCH 07/20] feat(api): Add argument that appends extra HTTP headers to a request Currently the only way to manipulate the headers for a request is to use `Gitlab.headers` attribute. However, this makes it very concurrently unsafe because the `Gitlab` object can be shared between multiple requests at the same time. Instead add a new keyword argument `extra_headers` which will update the headers dictionary with new values just before the request is sent. For example, this can be used to download a part of a artifacts file using the `Range` header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests Signed-off-by: Igor Ponomarev --- docs/api-usage-advanced.rst | 17 +++++++++++++++++ gitlab/client.py | 5 +++++ tests/unit/objects/test_job_artifacts.py | 24 ++++++++++++++++++++++++ tests/unit/test_gitlab_http_methods.py | 23 +++++++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/docs/api-usage-advanced.rst b/docs/api-usage-advanced.rst index ce18fd1e8..331d3446b 100644 --- a/docs/api-usage-advanced.rst +++ b/docs/api-usage-advanced.rst @@ -211,3 +211,20 @@ on your own, such as for nested API responses and ``Union`` return types. For ex if TYPE_CHECKING: assert isinstance(license["plan"], str) + +Per request HTTP headers override +--------------------------------- + +The ``extra_headers`` keyword argument can be used to add and override +the HTTP headers for a specific request. For example, it can be used do add ``Range`` +header to download a part of artifacts archive: + +.. code-block:: python + + import gitlab + + gl = gitlab.Gitlab(url, token) + project = gl.projects.get(1) + job = project.jobs.get(123) + + artifacts = job.artifacts(extra_headers={"Range": "bytes=0-9"}) diff --git a/gitlab/client.py b/gitlab/client.py index bf3ffbafc..87b324c34 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -654,6 +654,7 @@ def http_request( obey_rate_limit: bool = True, retry_transient_errors: Optional[bool] = None, max_retries: int = 10, + extra_headers: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> requests.Response: """Make an HTTP request to the Gitlab server. @@ -675,6 +676,7 @@ def http_request( or 52x responses. Defaults to False. max_retries: Max retries after 429 or transient errors, set to -1 to retry forever. Defaults to 10. + extra_headers: Add and override HTTP headers for the request. **kwargs: Extra options to send to the server (e.g. sudo) Returns: @@ -721,6 +723,9 @@ def http_request( send_data = self._backend.prepare_send_data(files, post_data, raw) opts["headers"]["Content-type"] = send_data.content_type + if extra_headers is not None: + opts["headers"].update(extra_headers) + retry = utils.Retry( max_retries=max_retries, obey_rate_limit=obey_rate_limit, diff --git a/tests/unit/objects/test_job_artifacts.py b/tests/unit/objects/test_job_artifacts.py index 8adcf8847..e7fd06f9e 100644 --- a/tests/unit/objects/test_job_artifacts.py +++ b/tests/unit/objects/test_job_artifacts.py @@ -35,6 +35,20 @@ def resp_project_artifacts_delete(): yield rsps +@pytest.fixture +def resp_job_artifact_bytes_range(binary_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/jobs/123/artifacts", + body=binary_content[:10], + content_type="application/octet-stream", + status=206, + match=[responses.matchers.header_matcher({"Range": "bytes=0-9"})], + ) + yield rsps + + def test_project_artifacts_delete(gl, resp_project_artifacts_delete): project = gl.projects.get(1, lazy=True) project.artifacts.delete() @@ -46,3 +60,13 @@ def test_project_artifacts_download_by_ref_name( project = gl.projects.get(1, lazy=True) artifacts = project.artifacts.download(ref_name=ref_name, job=job) assert artifacts == binary_content + + +def test_job_artifact_download_bytes_range( + gl, binary_content, resp_job_artifact_bytes_range +): + project = gl.projects.get(1, lazy=True) + job = project.jobs.get(123, lazy=True) + + artifacts = job.artifacts(extra_headers={"Range": "bytes=0-9"}) + assert len(artifacts) == 10 diff --git a/tests/unit/test_gitlab_http_methods.py b/tests/unit/test_gitlab_http_methods.py index fc8cd2d71..829643d99 100644 --- a/tests/unit/test_gitlab_http_methods.py +++ b/tests/unit/test_gitlab_http_methods.py @@ -117,6 +117,29 @@ def request_callback(request): assert len(responses.calls) == calls_before_success +@responses.activate +def test_http_request_extra_headers(gl): + path = "/projects/123/jobs/123456" + url = "http://localhost/api/v4" + path + + range_headers = {"Range": "bytes=0-99"} + + responses.add( + method=responses.GET, + url=url, + body=b"a" * 100, + status=206, + content_type="application/octet-stream", + match=helpers.MATCH_EMPTY_QUERY_PARAMS + + [responses.matchers.header_matcher(range_headers)], + ) + + http_r = gl.http_request("get", path, extra_headers=range_headers) + + assert http_r.status_code == 206 + assert len(http_r.content) == 100 + + @responses.activate @pytest.mark.parametrize( "exception", From 22f03bdc2bac92138225563415f5cf6fa36a5644 Mon Sep 17 00:00:00 2001 From: Eric Bishop Date: Thu, 2 Jan 2025 21:53:39 -0600 Subject: [PATCH 08/20] fix(files): add optional ref parameter for cli project-file raw (python-gitlab#3032) The ef parameter was removed in python-gitlab v4.8.0. This will add ef back as an optional parameter for the project-file raw cli command. --- gitlab/v4/objects/files.py | 1 + tests/functional/cli/test_cli_files.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/functional/cli/test_cli_files.py diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index e6ee27ba2..6d410b32e 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -277,6 +277,7 @@ def delete( # type: ignore[override] @cli.register_custom_action( cls_names="ProjectFileManager", required=("file_path",), + optional=("ref",), ) @exc.on_http_error(exc.GitlabGetError) def raw( diff --git a/tests/functional/cli/test_cli_files.py b/tests/functional/cli/test_cli_files.py new file mode 100644 index 000000000..405fbb21b --- /dev/null +++ b/tests/functional/cli/test_cli_files.py @@ -0,0 +1,21 @@ +def test_project_file_raw(gitlab_cli, project, project_file): + cmd = ["project-file", "raw", "--project-id", project.id, "--file-path", "README"] + ret = gitlab_cli(cmd) + assert ret.success + assert "Initial content" in ret.stdout + + +def test_project_file_raw_ref(gitlab_cli, project, project_file): + cmd = [ + "project-file", + "raw", + "--project-id", + project.id, + "--file-path", + "README", + "--ref", + "main", + ] + ret = gitlab_cli(cmd) + assert ret.success + assert "Initial content" in ret.stdout From cbd4263194fcbad9d6c11926862691f8df0dea6d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:29:21 +0000 Subject: [PATCH 09/20] chore(deps): update all non-major dependencies --- .github/workflows/docs.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/test.yml | 6 +++--- .pre-commit-config.yaml | 6 +++--- requirements-lint.txt | 12 ++++++------ requirements-precommit.txt | 2 +- requirements-test.txt | 12 ++++++------ requirements.txt | 2 +- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6282c4c32..031fb2c8c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,7 +34,7 @@ jobs: TOXENV: docs run: tox - name: Archive generated docs - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.6.0 with: name: html-docs path: build/sphinx/html/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29de997ea..dd6cc8a19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Python Semantic Release id: release - uses: python-semantic-release/python-semantic-release@v9.15.1 + uses: python-semantic-release/python-semantic-release@v9.16.1 with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1015601e6..cdfaee27b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,7 +15,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.0.0 + - uses: actions/stale@v9.1.0 with: stale-issue-label: "stale" stale-pr-label: "stale" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7f514f28..5af961e51 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,7 +78,7 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox -- --override-ini='log_cli=True' - name: Upload codecov coverage - uses: codecov/codecov-action@v5.1.1 + uses: codecov/codecov-action@v5.2.0 with: files: ./coverage.xml flags: ${{ matrix.toxenv }} @@ -101,7 +101,7 @@ jobs: TOXENV: cover run: tox - name: Upload codecov coverage - uses: codecov/codecov-action@v5.1.1 + uses: codecov/codecov-action@v5.2.0 with: files: ./coverage.xml flags: unit @@ -121,7 +121,7 @@ jobs: pip install -r requirements-test.txt - name: Build package run: python -m build -o dist/ - - uses: actions/upload-artifact@v4.4.3 + - uses: actions/upload-artifact@v4.6.0 with: name: dist path: dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01605556e..93f972397 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: hooks: - id: isort - repo: https://github.com/pycqa/pylint - rev: v3.3.2 + rev: v3.3.3 hooks: - id: pylint additional_dependencies: @@ -32,7 +32,7 @@ repos: - requests-toolbelt==1.0.0 files: 'gitlab/' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.0 + rev: v1.14.1 hooks: - id: mypy args: [] @@ -51,6 +51,6 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/maxbrunet/pre-commit-renovate - rev: 39.69.2 + rev: 39.122.1 hooks: - id: renovate-config-validator diff --git a/requirements-lint.txt b/requirements-lint.txt index 9b6b62c17..936496cbf 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -4,11 +4,11 @@ black==24.10.0 commitizen==4.1.0 flake8==7.1.1 isort==5.13.2 -mypy==1.14.0 -pylint==3.3.2 +mypy==1.14.1 +pylint==3.3.3 pytest==8.3.4 -responses==0.25.3 -respx==0.21.1 -types-PyYAML==6.0.12.20240917 +responses==0.25.6 +respx==0.22.0 +types-PyYAML==6.0.12.20241230 types-requests==2.32.0.20241016 -types-setuptools==75.6.0.20241126 +types-setuptools==75.8.0.20250110 diff --git a/requirements-precommit.txt b/requirements-precommit.txt index e88d27155..40a16fa94 100644 --- a/requirements-precommit.txt +++ b/requirements-precommit.txt @@ -1 +1 @@ -pre-commit==4.0.1 +pre-commit==4.1.0 diff --git a/requirements-test.txt b/requirements-test.txt index 629795a6d..9beda8f64 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,13 +1,13 @@ -r requirements.txt -anyio==4.7.0 +anyio==4.8.0 build==1.2.2.post1 -coverage==7.6.9 +coverage==7.6.10 pytest-console-scripts==1.4.1 pytest-cov==6.0.0 -pytest-github-actions-annotate-failures==0.2.0 +pytest-github-actions-annotate-failures==0.3.0 pytest==8.3.4 PyYaml==6.0.2 -responses==0.25.3 -respx==0.21.1 -trio==0.27.0 +responses==0.25.6 +respx==0.22.0 +trio==0.28.0 wheel==0.45.1 diff --git a/requirements.txt b/requirements.txt index aef5bc56d..21069f74f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ gql==3.5.0 -httpx==0.27.2 +httpx==0.28.1 requests==2.32.3 requests-toolbelt==1.0.0 From ba75c31e4d13927b6a3ab0ce427800d94e5eefb4 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 22 Jan 2025 11:24:06 -0800 Subject: [PATCH 10/20] chore: fix missing space in deprecation message --- gitlab/v4/objects/snippets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects/snippets.py b/gitlab/v4/objects/snippets.py index a1453c103..46c618e33 100644 --- a/gitlab/v4/objects/snippets.py +++ b/gitlab/v4/objects/snippets.py @@ -192,7 +192,7 @@ def public(self, **kwargs: Any) -> Union[RESTObjectList, List[RESTObject]]: """ utils.warn( message=( - "Gitlab.snippets.public() is deprecated and will be removed in a" + "Gitlab.snippets.public() is deprecated and will be removed in a " "future major version. Use Gitlab.snippets.list_public() instead." ), category=DeprecationWarning, From 0eb5eb0505c5b837a2d767cfa256a25b64ceb48b Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 22 Jan 2025 11:25:37 -0800 Subject: [PATCH 11/20] chore: fix warning being generated The CI shows a warning. Use `get_all=False` to resolve issue. --- tests/functional/api/test_gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/api/test_gitlab.py b/tests/functional/api/test_gitlab.py index af505c73b..50c6badd6 100644 --- a/tests/functional/api/test_gitlab.py +++ b/tests/functional/api/test_gitlab.py @@ -138,7 +138,7 @@ def test_template_gitlabciyml(gl, get_all_kwargs): def test_template_license(gl): - assert gl.licenses.list() + assert gl.licenses.list(get_all=False) license = gl.licenses.get( "bsd-2-clause", project="mytestproject", fullname="mytestfullname" ) From 95db680d012d73e7e505ee85db7128050ff0db6e Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 22 Jan 2025 11:26:47 -0800 Subject: [PATCH 12/20] chore: fix pytest deprecation pytest has changed the function argument name to `start_path` --- tests/functional/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index f2f31e52f..2d2815547 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -6,7 +6,7 @@ import time import uuid from subprocess import check_output -from typing import Optional +from typing import Optional, Sequence import pytest import requests @@ -145,7 +145,9 @@ def set_token(container: str, fixture_dir: pathlib.Path) -> str: return output -def pytest_report_collectionfinish(config, startdir, items): +def pytest_report_collectionfinish( + config: pytest.Config, start_path: pathlib.Path, items: Sequence[pytest.Item] +): return [ "", "Starting GitLab container.", From accd5aa757ba5215497c278da50d48f10ea5a258 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 22 Jan 2025 11:53:02 -0800 Subject: [PATCH 13/20] chore: resolve DeprecationWarning message in CI run Catch the DeprecationWarning in our test, as we expect it. --- tests/functional/api/test_snippets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/functional/api/test_snippets.py b/tests/functional/api/test_snippets.py index 7a71716fe..41a888d7d 100644 --- a/tests/functional/api/test_snippets.py +++ b/tests/functional/api/test_snippets.py @@ -24,7 +24,10 @@ def test_snippets(gl): assert content.decode() == "import gitlab" all_snippets = gl.snippets.list_all(get_all=True) - public_snippets = gl.snippets.public(get_all=True) + with pytest.warns( + DeprecationWarning, match=r"Gitlab.snippets.public\(\) is deprecated" + ): + public_snippets = gl.snippets.public(get_all=True) list_public_snippets = gl.snippets.list_public(get_all=True) assert isinstance(all_snippets, list) assert isinstance(list_public_snippets, list) From e8d6953ec06dbbd817852207abbbc74eab8a27cf Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Tue, 21 Jan 2025 08:11:01 -0800 Subject: [PATCH 14/20] chore(ci): set a 30 minute timeout for 'functional' tests Currently the functional API test takes around 17 minutes to run. And the functional CLI test takes around 12 minutes to run. Occasionally a job gets stuck and will sit until the default 360 minutes job timeout occurs. Now have a 30 minute timeout for the 'functional' tests. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5af961e51..08c78b7d8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,7 @@ jobs: run: tox --skip-missing-interpreters false functional: + timeout-minutes: 30 runs-on: ubuntu-24.04 strategy: matrix: From 9214b8371652be2371823b6f3d531eeea78364c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:01:43 +0000 Subject: [PATCH 15/20] chore(deps): update gitlab (#3088) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tests/functional/fixtures/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env index b687e0add..a25baaa07 100644 --- a/tests/functional/fixtures/.env +++ b/tests/functional/fixtures/.env @@ -1,4 +1,4 @@ GITLAB_IMAGE=gitlab/gitlab-ee -GITLAB_TAG=17.7.1-ee.0 +GITLAB_TAG=17.8.1-ee.0 GITLAB_RUNNER_IMAGE=gitlab/gitlab-runner -GITLAB_RUNNER_TAG=v17.7.1 +GITLAB_RUNNER_TAG=v17.8.3 From 939505b9c143939ba1e52c5cb920d8aa36596e19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 00:37:45 +0000 Subject: [PATCH 16/20] chore(deps): update all non-major dependencies --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 ++-- .pre-commit-config.yaml | 4 ++-- requirements-lint.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd6cc8a19..a9be4bea8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Python Semantic Release id: release - uses: python-semantic-release/python-semantic-release@v9.16.1 + uses: python-semantic-release/python-semantic-release@v9.17.0 with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 08c78b7d8..c448fce2b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox -- --override-ini='log_cli=True' - name: Upload codecov coverage - uses: codecov/codecov-action@v5.2.0 + uses: codecov/codecov-action@v5.3.1 with: files: ./coverage.xml flags: ${{ matrix.toxenv }} @@ -102,7 +102,7 @@ jobs: TOXENV: cover run: tox - name: Upload codecov coverage - uses: codecov/codecov-action@v5.2.0 + uses: codecov/codecov-action@v5.3.1 with: files: ./coverage.xml flags: unit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93f972397..7b324807e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: hooks: - id: black - repo: https://github.com/commitizen-tools/commitizen - rev: v4.1.0 + rev: v4.1.1 hooks: - id: commitizen stages: [commit-msg] @@ -51,6 +51,6 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/maxbrunet/pre-commit-renovate - rev: 39.122.1 + rev: 39.134.0 hooks: - id: renovate-config-validator diff --git a/requirements-lint.txt b/requirements-lint.txt index 936496cbf..876ff644b 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,7 +1,7 @@ -r requirements.txt argcomplete==2.0.0 black==24.10.0 -commitizen==4.1.0 +commitizen==4.1.1 flake8==7.1.1 isort==5.13.2 mypy==1.14.1 From de29503262b7626421f3bffeea3ff073e63e3865 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 22 Jan 2025 16:59:58 -0500 Subject: [PATCH 17/20] fix(api): return the new commit when calling cherry_pick --- gitlab/v4/objects/commits.py | 9 +++++++-- tests/functional/api/test_repository.py | 22 ++++++++++++++++++++++ tests/unit/objects/test_commits.py | 25 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/gitlab/v4/objects/commits.py b/gitlab/v4/objects/commits.py index 0cb0a127a..e7c8164b7 100644 --- a/gitlab/v4/objects/commits.py +++ b/gitlab/v4/objects/commits.py @@ -48,7 +48,9 @@ def diff(self, **kwargs: Any) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]: @cli.register_custom_action(cls_names="ProjectCommit", required=("branch",)) @exc.on_http_error(exc.GitlabCherryPickError) - def cherry_pick(self, branch: str, **kwargs: Any) -> None: + def cherry_pick( + self, branch: str, **kwargs: Any + ) -> Union[Dict[str, Any], requests.Response]: """Cherry-pick a commit into a branch. Args: @@ -58,10 +60,13 @@ def cherry_pick(self, branch: str, **kwargs: Any) -> None: Raises: GitlabAuthenticationError: If authentication is not correct GitlabCherryPickError: If the cherry-pick could not be performed + + Returns: + The new commit data (*not* a RESTObject) """ path = f"{self.manager.path}/{self.encoded_id}/cherry_pick" post_data = {"branch": branch} - self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) @cli.register_custom_action(cls_names="ProjectCommit", optional=("type",)) @exc.on_http_error(exc.GitlabGetError) diff --git a/tests/functional/api/test_repository.py b/tests/functional/api/test_repository.py index b4e80c9f8..4376c64c5 100644 --- a/tests/functional/api/test_repository.py +++ b/tests/functional/api/test_repository.py @@ -165,6 +165,28 @@ def test_commit_discussion(project): note_from_get.delete() +def test_cherry_pick_commit(project): + commits = project.commits.list() + commit = commits[1] + parent_commit = commit.parent_ids[0] + + # create a branch to cherry pick onto + project.branches.create( + { + "branch": "test", + "ref": parent_commit, + } + ) + cherry_pick_commit = commit.cherry_pick(branch="test") + + expected_message = f"{commit.message}\n\n(cherry picked from commit {commit.id})" + assert cherry_pick_commit["message"].startswith(expected_message) + + with pytest.raises(gitlab.GitlabCherryPickError): + # Two cherry pick attempts should raise GitlabCherryPickError + commit.cherry_pick(branch="test") + + def test_revert_commit(project): commit = project.commits.list()[0] revert_commit = commit.revert(branch="main") diff --git a/tests/unit/objects/test_commits.py b/tests/unit/objects/test_commits.py index 5b0270c6e..b9aa92a6d 100644 --- a/tests/unit/objects/test_commits.py +++ b/tests/unit/objects/test_commits.py @@ -37,6 +37,12 @@ def resp_commit(): "short_id": "8b090c1b", "title": 'Revert "Initial commit"', } + cherry_pick_content = { + "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", + "short_id": "8b090c1b", + "title": "Initial commit", + "message": "Initial commit\n\n\n(cherry picked from commit 6b2257eabcec3db1f59dafbd84935e3caea04235)", + } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( @@ -53,6 +59,13 @@ def resp_commit(): content_type="application/json", status=200, ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea/cherry_pick", + json=cherry_pick_content, + content_type="application/json", + status=200, + ) yield rsps @@ -118,6 +131,18 @@ def test_create_commit(project, resp_create_commit): assert commit.title == data["commit_message"] +def test_cherry_pick_commit(project, resp_commit): + commit = project.commits.get("6b2257ea", lazy=True) + cherry_pick_commit = commit.cherry_pick(branch="main") + + assert cherry_pick_commit["short_id"] == "8b090c1b" + assert cherry_pick_commit["title"] == "Initial commit" + assert ( + cherry_pick_commit["message"] + == "Initial commit\n\n\n(cherry picked from commit 6b2257eabcec3db1f59dafbd84935e3caea04235)" + ) + + def test_revert_commit(project, resp_commit): commit = project.commits.get("6b2257ea", lazy=True) revert_commit = commit.revert(branch="main") From 175b355d84d54a71f15fe3601c5275dc35984b9b Mon Sep 17 00:00:00 2001 From: Sachin Singh Date: Wed, 22 Jan 2025 22:36:33 +0000 Subject: [PATCH 18/20] feat(api): add support for external status check --- docs/api-objects.rst | 1 + docs/gl_objects/status_checks.rst | 57 ++++++++++ gitlab/v4/objects/__init__.py | 1 + gitlab/v4/objects/merge_requests.py | 2 + gitlab/v4/objects/projects.py | 2 + gitlab/v4/objects/status_checks.py | 52 ++++++++++ tests/functional/api/test_projects.py | 16 +++ tests/unit/objects/test_status_checks.py | 127 +++++++++++++++++++++++ 8 files changed, 258 insertions(+) create mode 100644 docs/gl_objects/status_checks.rst create mode 100644 gitlab/v4/objects/status_checks.py create mode 100644 tests/unit/objects/test_status_checks.py diff --git a/docs/api-objects.rst b/docs/api-objects.rst index 4868983e6..c8d4b7891 100644 --- a/docs/api-objects.rst +++ b/docs/api-objects.rst @@ -63,6 +63,7 @@ API examples gl_objects/settings gl_objects/snippets gl_objects/statistics + gl_objects/status_checks gl_objects/system_hooks gl_objects/templates gl_objects/todos diff --git a/docs/gl_objects/status_checks.rst b/docs/gl_objects/status_checks.rst new file mode 100644 index 000000000..71d0c1abf --- /dev/null +++ b/docs/gl_objects/status_checks.rst @@ -0,0 +1,57 @@ +####################### +External Status Checks +####################### + +Manage external status checks for projects and merge requests. + + +Project external status checks +=============================== + +Reference +--------- + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectExternalStatusCheck` + + :class:`gitlab.v4.objects.ProjectExternalStatusCheckManager` + + :attr:`gitlab.v4.objects.Project.external_status_checks` + +* GitLab API: https://docs.gitlab.com/ee/api/status_checks.html + +Examples +--------- + +List external status checks for a project:: + + status_checks = project.external_status_checks.list() + +Create an external status check with shared secret:: + + status_checks = project.external_status_checks.create({ + "name": "mr_blocker", + "external_url": "https://example.com/mr-status-check", + "shared_secret": "secret-string" + }) + +Create an external status check with shared secret for protected branches:: + + protected_branch = project.protectedbranches.get('main') + + status_check = project.external_status_checks.create({ + "name": "mr_blocker", + "external_url": "https://example.com/mr-status-check", + "shared_secret": "secret-string", + "protected_branch_ids": [protected_branch.id] + }) + + +Update an external status check:: + + status_check.external_url = "https://example.com/mr-blocker" + status_check.save() + +Delete an external status check:: + + status_check.delete(status_check_id) + diff --git a/gitlab/v4/objects/__init__.py b/gitlab/v4/objects/__init__.py index 3aa992203..7932080ac 100644 --- a/gitlab/v4/objects/__init__.py +++ b/gitlab/v4/objects/__init__.py @@ -68,6 +68,7 @@ from .sidekiq import * from .snippets import * from .statistics import * +from .status_checks import * from .tags import * from .templates import * from .todos import * diff --git a/gitlab/v4/objects/merge_requests.py b/gitlab/v4/objects/merge_requests.py index e29ab2b28..30ddc11f5 100644 --- a/gitlab/v4/objects/merge_requests.py +++ b/gitlab/v4/objects/merge_requests.py @@ -44,6 +44,7 @@ from .notes import ProjectMergeRequestNoteManager # noqa: F401 from .pipelines import ProjectMergeRequestPipelineManager # noqa: F401 from .reviewers import ProjectMergeRequestReviewerDetailManager +from .status_checks import ProjectMergeRequestStatusCheckManager __all__ = [ "MergeRequest", @@ -167,6 +168,7 @@ class ProjectMergeRequest( resourcemilestoneevents: ProjectMergeRequestResourceMilestoneEventManager resourcestateevents: ProjectMergeRequestResourceStateEventManager reviewer_details: ProjectMergeRequestReviewerDetailManager + status_checks: ProjectMergeRequestStatusCheckManager @cli.register_custom_action(cls_names="ProjectMergeRequest") @exc.on_http_error(exc.GitlabMROnBuildSuccessError) diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index daf69c923..b2e86a65d 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -104,6 +104,7 @@ ProjectAdditionalStatisticsManager, ProjectIssuesStatisticsManager, ) +from .status_checks import ProjectExternalStatusCheckManager # noqa: F401 from .tags import ProjectProtectedTagManager, ProjectTagManager # noqa: F401 from .templates import ( # noqa: F401 ProjectDockerfileTemplateManager, @@ -253,6 +254,7 @@ class Project( secure_files: ProjectSecureFileManager services: ProjectServiceManager snippets: ProjectSnippetManager + external_status_checks: ProjectExternalStatusCheckManager storage: "ProjectStorageManager" tags: ProjectTagManager triggers: ProjectTriggerManager diff --git a/gitlab/v4/objects/status_checks.py b/gitlab/v4/objects/status_checks.py new file mode 100644 index 000000000..045b57260 --- /dev/null +++ b/gitlab/v4/objects/status_checks.py @@ -0,0 +1,52 @@ +from gitlab.base import RESTManager, RESTObject +from gitlab.mixins import ( + CreateMixin, + DeleteMixin, + ListMixin, + ObjectDeleteMixin, + SaveMixin, + UpdateMethod, + UpdateMixin, +) +from gitlab.types import ArrayAttribute, RequiredOptional + +__all__ = [ + "ProjectExternalStatusCheck", + "ProjectExternalStatusCheckManager", + "ProjectMergeRequestStatusCheck", + "ProjectMergeRequestStatusCheckManager", +] + + +class ProjectExternalStatusCheck(SaveMixin, ObjectDeleteMixin, RESTObject): + pass + + +class ProjectExternalStatusCheckManager( + ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager +): + _path = "/projects/{project_id}/external_status_checks" + _obj_cls = ProjectExternalStatusCheck + _from_parent_attrs = {"project_id": "id"} + _create_attrs = RequiredOptional( + required=("name", "external_url"), + optional=("shared_secret", "protected_branch_ids"), + ) + _update_attrs = RequiredOptional( + optional=("name", "external_url", "shared_secret", "protected_branch_ids") + ) + _types = {"protected_branch_ids": ArrayAttribute} + + +class ProjectMergeRequestStatusCheck(SaveMixin, RESTObject): + pass + + +class ProjectMergeRequestStatusCheckManager(ListMixin, RESTManager): + _path = "/projects/{project_id}/merge_requests/{merge_request_iid}/status_checks" + _obj_cls = ProjectMergeRequestStatusCheck + _from_parent_attrs = {"project_id": "project_id", "merge_request_iid": "iid"} + _update_attrs = RequiredOptional( + required=("sha", "external_status_check_id", "status") + ) + _update_method = UpdateMethod.POST diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py index c96d93a13..18c850680 100644 --- a/tests/functional/api/test_projects.py +++ b/tests/functional/api/test_projects.py @@ -399,3 +399,19 @@ def test_project_transfer(gl, project, group): project = gl.projects.get(project.id) assert project.namespace["path"] == gl.user.username + + +@pytest.mark.gitlab_premium +def test_project_external_status_check_create(gl, project): + status_check = project.external_status_checks.create( + {"name": "MR blocker", "external_url": "https://example.com/mr-blocker"} + ) + assert status_check.name == "MR blocker" + assert status_check.external_url == "https://example.com/mr-blocker" + + +@pytest.mark.gitlab_premium +def test_project_external_status_check_list(gl, project): + status_checks = project.external_status_checks.list() + + assert len(status_checks) == 1 diff --git a/tests/unit/objects/test_status_checks.py b/tests/unit/objects/test_status_checks.py new file mode 100644 index 000000000..14d1e73d4 --- /dev/null +++ b/tests/unit/objects/test_status_checks.py @@ -0,0 +1,127 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/status_checks.html +""" + +import pytest +import responses + + +@pytest.fixture +def external_status_check(): + return { + "id": 1, + "name": "MR blocker", + "project_id": 1, + "external_url": "https://example.com/mr-blocker", + "hmac": True, + "protected_branches": [ + { + "id": 1, + "project_id": 1, + "name": "main", + "created_at": "2020-10-12T14:04:50.787Z", + "updated_at": "2020-10-12T14:04:50.787Z", + "code_owner_approval_required": False, + } + ], + } + + +@pytest.fixture +def updated_external_status_check(): + return { + "id": 1, + "name": "Updated MR blocker", + "project_id": 1, + "external_url": "https://example.com/mr-blocker", + "hmac": True, + "protected_branches": [ + { + "id": 1, + "project_id": 1, + "name": "main", + "created_at": "2020-10-12T14:04:50.787Z", + "updated_at": "2020-10-12T14:04:50.787Z", + "code_owner_approval_required": False, + } + ], + } + + +@pytest.fixture +def resp_list_external_status_checks(external_status_check): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/external_status_checks", + json=[external_status_check], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_external_status_checks(external_status_check): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/external_status_checks", + json=external_status_check, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_update_external_status_checks(updated_external_status_check): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/groups/1/external_status_checks", + json=updated_external_status_check, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_external_status_checks(): + content = [] + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/projects/1/external_status_checks/1", + status=204, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/external_status_checks", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_external_status_checks(gl, resp_list_external_status_checks): + status_checks = gl.projects.get(1, lazy=True).external_status_checks.list() + assert len(status_checks) == 1 + assert status_checks[0].name == "MR blocker" + + +def test_create_external_status_checks(gl, resp_create_external_status_checks): + access_token = gl.projects.get(1, lazy=True).external_status_checks.create( + {"name": "MR blocker", "external_url": "https://example.com/mr-blocker"} + ) + assert access_token.name == "MR blocker" + assert access_token.external_url == "https://example.com/mr-blocker" + + +def test_delete_external_status_checks(gl, resp_delete_external_status_checks): + gl.projects.get(1, lazy=True).external_status_checks.delete(1) + status_checks = gl.projects.get(1, lazy=True).external_status_checks.list() + assert len(status_checks) == 0 From 36d9b24ff27d8df514c1beebd0fff8ad000369b7 Mon Sep 17 00:00:00 2001 From: Igor Ponomarev Date: Fri, 17 Jan 2025 11:58:19 +0000 Subject: [PATCH 19/20] feat(api): Narrow down return type of ProjectFileManager.raw using typing.overload This is equivalent to the changes in 44fd9dc1176a2c5529c45cc3186c0e775026175e but for `ProjectFileManager.raw` method that I must have missed in the original commit. Signed-off-by: Igor Ponomarev --- gitlab/v4/objects/files.py | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index 6d410b32e..ce2193c2c 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -5,7 +5,9 @@ Dict, Iterator, List, + Literal, Optional, + overload, Tuple, TYPE_CHECKING, Union, @@ -274,6 +276,45 @@ def delete( # type: ignore[override] data = {"branch": branch, "commit_message": commit_message} self.gitlab.http_delete(path, query_data=data, **kwargs) + @overload + def raw( + self, + file_path: str, + ref: Optional[str] = None, + streamed: Literal[False] = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> bytes: ... + + @overload + def raw( + self, + file_path: str, + ref: Optional[str] = None, + streamed: bool = False, + action: None = None, + chunk_size: int = 1024, + *, + iterator: Literal[True] = True, + **kwargs: Any, + ) -> Iterator[Any]: ... + + @overload + def raw( + self, + file_path: str, + ref: Optional[str] = None, + streamed: Literal[True] = True, + action: Optional[Callable[[bytes], None]] = None, + chunk_size: int = 1024, + *, + iterator: Literal[False] = False, + **kwargs: Any, + ) -> None: ... + @cli.register_custom_action( cls_names="ProjectFileManager", required=("file_path",), From 30f470b1bb6876db815e8f6694583315a6a68778 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 28 Jan 2025 00:55:06 +0000 Subject: [PATCH 20/20] chore: release v5.4.0 --- CHANGELOG.md | 143 +++++++++++++++++++++++++++++++++++++++++++++ gitlab/_version.py | 2 +- 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 842aacf74..4b0ac9040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,149 @@ # CHANGELOG +## v5.4.0 (2025-01-28) + +### Bug Fixes + +- **api**: Make type ignores more specific where possible + ([`e3cb806`](https://github.com/python-gitlab/python-gitlab/commit/e3cb806dc368af0a495087531ee94892d3f240ce)) + +Instead of using absolute ignore `# type: ignore` use a more specific ignores like `# type: + ignore[override]`. This might help in the future where a new bug might be introduced and get + ignored by a general ignore comment but not a more specific one. + +Signed-off-by: Igor Ponomarev + +- **api**: Return the new commit when calling cherry_pick + ([`de29503`](https://github.com/python-gitlab/python-gitlab/commit/de29503262b7626421f3bffeea3ff073e63e3865)) + +- **files**: Add optional ref parameter for cli project-file raw (python-gitlab#3032) + ([`22f03bd`](https://github.com/python-gitlab/python-gitlab/commit/22f03bdc2bac92138225563415f5cf6fa36a5644)) + +The ef parameter was removed in python-gitlab v4.8.0. This will add ef back as an optional parameter + for the project-file raw cli command. + +### Chores + +- Fix missing space in deprecation message + ([`ba75c31`](https://github.com/python-gitlab/python-gitlab/commit/ba75c31e4d13927b6a3ab0ce427800d94e5eefb4)) + +- Fix pytest deprecation + ([`95db680`](https://github.com/python-gitlab/python-gitlab/commit/95db680d012d73e7e505ee85db7128050ff0db6e)) + +pytest has changed the function argument name to `start_path` + +- Fix warning being generated + ([`0eb5eb0`](https://github.com/python-gitlab/python-gitlab/commit/0eb5eb0505c5b837a2d767cfa256a25b64ceb48b)) + +The CI shows a warning. Use `get_all=False` to resolve issue. + +- Resolve DeprecationWarning message in CI run + ([`accd5aa`](https://github.com/python-gitlab/python-gitlab/commit/accd5aa757ba5215497c278da50d48f10ea5a258)) + +Catch the DeprecationWarning in our test, as we expect it. + +- **ci**: Set a 30 minute timeout for 'functional' tests + ([`e8d6953`](https://github.com/python-gitlab/python-gitlab/commit/e8d6953ec06dbbd817852207abbbc74eab8a27cf)) + +Currently the functional API test takes around 17 minutes to run. And the functional CLI test takes + around 12 minutes to run. + +Occasionally a job gets stuck and will sit until the default 360 minutes job timeout occurs. + +Now have a 30 minute timeout for the 'functional' tests. + +- **deps**: Update all non-major dependencies + ([`939505b`](https://github.com/python-gitlab/python-gitlab/commit/939505b9c143939ba1e52c5cb920d8aa36596e19)) + +- **deps**: Update all non-major dependencies + ([`cbd4263`](https://github.com/python-gitlab/python-gitlab/commit/cbd4263194fcbad9d6c11926862691f8df0dea6d)) + +- **deps**: Update gitlab ([#3088](https://github.com/python-gitlab/python-gitlab/pull/3088), + [`9214b83`](https://github.com/python-gitlab/python-gitlab/commit/9214b8371652be2371823b6f3d531eeea78364c7)) + +Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> + +- **deps**: Update gitlab/gitlab-ee docker tag to v17.7.1-ee.0 + ([#3082](https://github.com/python-gitlab/python-gitlab/pull/3082), + [`1e95944`](https://github.com/python-gitlab/python-gitlab/commit/1e95944119455875bd239752cdf0fe5cc27707ea)) + +Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> + +- **deps**: Update mypy to 1.14 and resolve issues + ([`671e711`](https://github.com/python-gitlab/python-gitlab/commit/671e711c341d28ae0bc61ccb12d2e986353473fd)) + +mypy 1.14 has a change to Enum Membership Semantics: + https://mypy.readthedocs.io/en/latest/changelog.html + +Resolve the issues with Enum and typing, and update mypy to 1.14 + +- **test**: Prevent 'job_with_artifact' fixture running forever + ([`e4673d8`](https://github.com/python-gitlab/python-gitlab/commit/e4673d8aeaf97b9ad5d2500e459526b4cf494547)) + +Previously the 'job_with_artifact' fixture could run forever. Now give it up to 60 seconds to + complete before failing. + +### Continuous Integration + +- Use gitlab-runner:v17.7.1 for the CI + ([`2dda9dc`](https://github.com/python-gitlab/python-gitlab/commit/2dda9dc149668a99211daaa1981bb1f422c63880)) + +The `latest` gitlab-runner image does not have the `gitlab-runner` user and it causes our tests to + fail. + +Closes: #3091 + +### Features + +- **api**: Add argument that appends extra HTTP headers to a request + ([`fb07b5c`](https://github.com/python-gitlab/python-gitlab/commit/fb07b5cfe1d986c3a7cd7879b11ecc43c75542b7)) + +Currently the only way to manipulate the headers for a request is to use `Gitlab.headers` attribute. + However, this makes it very concurrently unsafe because the `Gitlab` object can be shared between + multiple requests at the same time. + +Instead add a new keyword argument `extra_headers` which will update the headers dictionary with new + values just before the request is sent. + +For example, this can be used to download a part of a artifacts file using the `Range` header: + https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests + +Signed-off-by: Igor Ponomarev + +- **api**: Add support for external status check + ([`175b355`](https://github.com/python-gitlab/python-gitlab/commit/175b355d84d54a71f15fe3601c5275dc35984b9b)) + +- **api**: Narrow down return type of download methods using typing.overload + ([`44fd9dc`](https://github.com/python-gitlab/python-gitlab/commit/44fd9dc1176a2c5529c45cc3186c0e775026175e)) + +Currently the download methods such as `ProjectJob.artifacts` have return type set to + `Optional[Union[bytes, Iterator[Any]]]` which means they return either `None` or `bytes` or + `Iterator[Any]`. + +However, the actual return type is determined by the passed `streamed` and `iterator` arguments. + Using `@typing.overload` decorator it is possible to return a single type based on the passed + arguments. + +Add overloads in the following order to all download methods: + +1. If `streamed=False` and `iterator=False` return `bytes`. This is the default argument values + therefore it should be first as it will be used to lookup default arguments. 2. If `iterator=True` + return `Iterator[Any]`. This can be combined with both `streamed=True` and `streamed=False`. 3. If + `streamed=True` and `iterator=False` return `None`. In this case `action` argument can be set to a + callable that accepts `bytes`. + +Signed-off-by: Igor Ponomarev + +- **api**: Narrow down return type of ProjectFileManager.raw using typing.overload + ([`36d9b24`](https://github.com/python-gitlab/python-gitlab/commit/36d9b24ff27d8df514c1beebd0fff8ad000369b7)) + +This is equivalent to the changes in 44fd9dc1176a2c5529c45cc3186c0e775026175e but for + `ProjectFileManager.raw` method that I must have missed in the original commit. + +Signed-off-by: Igor Ponomarev + + ## v5.3.1 (2025-01-07) ### Bug Fixes diff --git a/gitlab/_version.py b/gitlab/_version.py index b772bcf4f..f4415c059 100644 --- a/gitlab/_version.py +++ b/gitlab/_version.py @@ -3,4 +3,4 @@ __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __title__ = "python-gitlab" -__version__ = "5.3.1" +__version__ = "5.4.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