From a094e497c43c45fbdf2c436992ed06497045622d Mon Sep 17 00:00:00 2001 From: Gerry Pratama Date: Wed, 4 Dec 2024 23:27:40 -0700 Subject: [PATCH 1/5] feat(api): Added project template classes to templates.py --- gitlab/v4/objects/templates.py | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/gitlab/v4/objects/templates.py b/gitlab/v4/objects/templates.py index bbe2ae6c1..343ac4ca7 100644 --- a/gitlab/v4/objects/templates.py +++ b/gitlab/v4/objects/templates.py @@ -12,6 +12,18 @@ "GitlabciymlManager", "License", "LicenseManager", + "ProjectDockerfileTemplate", + "ProjectDockerfileTemplateManager", + "ProjectGitignoreTemplate", + "ProjectGitignoreTemplateManager", + "ProjectGitlabciymlTemplate", + "ProjectGitlabciymlTemplateManager", + "ProjectIssueTemplate", + "ProjectIssueTemplateManager", + "ProjectLicenseTemplate", + "ProjectLicenseTemplateManager", + "ProjectMergeRequestTemplate", + "ProjectMergeRequestTemplateManager", ] @@ -65,3 +77,95 @@ class LicenseManager(RetrieveMixin, RESTManager): def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> License: return cast(License, super().get(id=id, lazy=lazy, **kwargs)) + + +class ProjectDockerfileTemplate(RESTObject): + _id_attr = "name" + + +class ProjectDockerfileTemplateManager(RetrieveMixin, RESTManager): + _path = "/projects/{project_id}/templates/dockerfiles" + _obj_cls = ProjectDockerfileTemplate + _from_parent_attrs = {"project_id": "id"} + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectDockerfileTemplate: + return cast(ProjectDockerfileTemplate, super().get(id=id, lazy=lazy, **kwargs)) + + +class ProjectGitignoreTemplate(RESTObject): + _id_attr = "name" + + +class ProjectGitignoreTemplateManager(RetrieveMixin, RESTManager): + _path = "/projects/{project_id}/templates/gitignores" + _obj_cls = ProjectGitignoreTemplate + _from_parent_attrs = {"project_id": "id"} + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectGitignoreTemplate: + return cast(ProjectGitignoreTemplate, super().get(id=id, lazy=lazy, **kwargs)) + + +class ProjectGitlabciymlTemplate(RESTObject): + _id_attr = "name" + + +class ProjectGitlabciymlTemplateManager(RetrieveMixin, RESTManager): + _path = "/projects/{project_id}/templates/gitlab_ci_ymls" + _obj_cls = ProjectGitlabciymlTemplate + _from_parent_attrs = {"project_id": "id"} + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectGitlabciymlTemplate: + return cast(ProjectGitlabciymlTemplate, super().get(id=id, lazy=lazy, **kwargs)) + + +class ProjectLicenseTemplate(RESTObject): + _id_attr = "key" + + +class ProjectLicenseTemplateManager(RetrieveMixin, RESTManager): + _path = "/projects/{project_id}/templates/licenses" + _obj_cls = ProjectLicenseTemplate + _from_parent_attrs = {"project_id": "id"} + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectLicenseTemplate: + return cast(ProjectLicenseTemplate, super().get(id=id, lazy=lazy, **kwargs)) + + +class ProjectIssueTemplate(RESTObject): + _id_attr = "name" + + +class ProjectIssueTemplateManager(RetrieveMixin, RESTManager): + _path = "/projects/{project_id}/templates/issues" + _obj_cls = ProjectIssueTemplate + _from_parent_attrs = {"project_id": "id"} + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectIssueTemplate: + return cast(ProjectIssueTemplate, super().get(id=id, lazy=lazy, **kwargs)) + + +class ProjectMergeRequestTemplate(RESTObject): + _id_attr = "name" + + +class ProjectMergeRequestTemplateManager(RetrieveMixin, RESTManager): + _path = "/projects/{project_id}/templates/merge_requests" + _obj_cls = ProjectMergeRequestTemplate + _from_parent_attrs = {"project_id": "id"} + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectMergeRequestTemplate: + return cast( + ProjectMergeRequestTemplate, super().get(id=id, lazy=lazy, **kwargs) + ) From 9e9f70651e9b8fb1e02df1e15ec0348be1d5b4f9 Mon Sep 17 00:00:00 2001 From: Gerry Pratama Date: Thu, 5 Dec 2024 01:30:56 -0700 Subject: [PATCH 2/5] feat(api): Added project template managers to Project in project.py --- 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 a3e5efb65..937fd7221 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -100,6 +100,14 @@ ProjectIssuesStatisticsManager, ) from .tags import ProjectProtectedTagManager, ProjectTagManager # noqa: F401 +from .templates import ( # noqa: F401 + ProjectDockerfileTemplateManager, + ProjectGitignoreTemplateManager, + ProjectGitlabciymlTemplateManager, + ProjectIssueTemplateManager, + ProjectLicenseTemplateManager, + ProjectMergeRequestTemplateManager, +) from .triggers import ProjectTriggerManager # noqa: F401 from .users import ProjectUserManager # noqa: F401 from .variables import ProjectVariableManager # noqa: F401 @@ -189,27 +197,33 @@ class Project( customattributes: ProjectCustomAttributeManager deployments: ProjectDeploymentManager deploytokens: ProjectDeployTokenManager + dockerfile_templates: ProjectDockerfileTemplateManager environments: ProjectEnvironmentManager events: ProjectEventManager exports: ProjectExportManager files: ProjectFileManager forks: "ProjectForkManager" generic_packages: GenericPackageManager + gitignore_templates: ProjectGitignoreTemplateManager + gitlabciyml_templates: ProjectGitlabciymlTemplateManager groups: ProjectGroupManager hooks: ProjectHookManager imports: ProjectImportManager integrations: ProjectIntegrationManager invitations: ProjectInvitationManager issues: ProjectIssueManager + issue_templates: ProjectIssueTemplateManager issues_statistics: ProjectIssuesStatisticsManager iterations: ProjectIterationManager jobs: ProjectJobManager job_token_scope: ProjectJobTokenScopeManager keys: ProjectKeyManager labels: ProjectLabelManager + license_templates: ProjectLicenseTemplateManager members: ProjectMemberManager members_all: ProjectMemberAllManager mergerequests: ProjectMergeRequestManager + merge_request_templates: ProjectMergeRequestTemplateManager merge_trains: ProjectMergeTrainManager milestones: ProjectMilestoneManager notes: ProjectNoteManager From 24724f37e3f668814b4f72b3a93cba69895cd48e Mon Sep 17 00:00:00 2001 From: Gerry Pratama Date: Thu, 5 Dec 2024 02:43:51 -0700 Subject: [PATCH 3/5] docs(merge_requests): Add example of creating mr with description template --- docs/gl_objects/merge_requests.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/gl_objects/merge_requests.rst b/docs/gl_objects/merge_requests.rst index 8264669e6..917cb8cc6 100644 --- a/docs/gl_objects/merge_requests.rst +++ b/docs/gl_objects/merge_requests.rst @@ -95,6 +95,7 @@ Get a single MR:: mr = project.mergerequests.get(mr_iid) Get MR reviewer details:: + mr = project.mergerequests.get(mr_iid) reviewers = mr.reviewer_details.list() @@ -105,6 +106,13 @@ Create a MR:: 'title': 'merge cool feature', 'labels': ['label1', 'label2']}) + # Use a project MR description template + mr_description_template = project.merge_request_templates.get("Default") + mr = project.mergerequests.create({'source_branch': 'cool_feature', + 'target_branch': 'main', + 'title': 'merge cool feature', + 'description': mr_description_template.content}) + Update a MR:: mr.description = 'New description' From a71be7248e7590eabf0d9a6f504e68e01c642178 Mon Sep 17 00:00:00 2001 From: Gerry Pratama Date: Fri, 6 Dec 2024 22:09:59 -0700 Subject: [PATCH 4/5] test(templates): Added unit tests for templates --- tests/unit/objects/test_templates.py | 106 +++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/unit/objects/test_templates.py diff --git a/tests/unit/objects/test_templates.py b/tests/unit/objects/test_templates.py new file mode 100644 index 000000000..fc9058d74 --- /dev/null +++ b/tests/unit/objects/test_templates.py @@ -0,0 +1,106 @@ +""" +Gitlab API: +https://docs.gitlab.com/ce/api/templates/dockerfiles.html +https://docs.gitlab.com/ce/api/templates/gitignores.html +https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html +https://docs.gitlab.com/ce/api/templates/licenses.html +https://docs.gitlab.com/ce/api/project_templates.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ( + Dockerfile, + Gitignore, + Gitlabciyml, + License, + ProjectDockerfileTemplate, + ProjectGitignoreTemplate, + ProjectGitlabciymlTemplate, + ProjectIssueTemplate, + ProjectLicenseTemplate, + ProjectMergeRequestTemplate, +) + + +@pytest.mark.parametrize( + "tmpl, tmpl_mgr, tmpl_path", + [ + (Dockerfile, "dockerfiles", "dockerfiles"), + (Gitignore, "gitignores", "gitignores"), + (Gitlabciyml, "gitlabciymls", "gitlab_ci_ymls"), + (License, "licenses", "licenses"), + ], + ids=[ + "dockerfile", + "gitignore", + "gitlabciyml", + "license", + ], +) +def test_get_template(gl, tmpl, tmpl_mgr, tmpl_path): + tmpl_id = "sample" + tmpl_content = {"name": tmpl_id, "content": "Sample template content"} + + # License templates have 'key' as the id attribute, so ensure + # this is included in the response content + if tmpl == License: + tmpl_id = "smpl" + tmpl_content.update({"key": tmpl_id}) + + path = f"templates/{tmpl_path}/{tmpl_id}" + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=f"http://localhost/api/v4/{path}", + json=tmpl_content, + ) + + template = getattr(gl, tmpl_mgr).get(tmpl_id) + + assert isinstance(template, tmpl) + assert getattr(template, template._id_attr) == tmpl_id + + +@pytest.mark.parametrize( + "tmpl, tmpl_mgr, tmpl_path", + [ + (ProjectDockerfileTemplate, "dockerfile_templates", "dockerfiles"), + (ProjectGitignoreTemplate, "gitignore_templates", "gitignores"), + (ProjectGitlabciymlTemplate, "gitlabciyml_templates", "gitlab_ci_ymls"), + (ProjectLicenseTemplate, "license_templates", "licenses"), + (ProjectIssueTemplate, "issue_templates", "issues"), + (ProjectMergeRequestTemplate, "merge_request_templates", "merge_requests"), + ], + ids=[ + "dockerfile", + "gitignore", + "gitlabciyml", + "license", + "issue", + "mergerequest", + ], +) +def test_get_project_template(project, tmpl, tmpl_mgr, tmpl_path): + tmpl_id = "sample" + tmpl_content = {"name": tmpl_id, "content": "Sample template content"} + + # ProjectLicenseTemplate templates have 'key' as the id attribute, so ensure + # this is included in the response content + if tmpl == ProjectLicenseTemplate: + tmpl_id = "smpl" + tmpl_content.update({"key": tmpl_id}) + + path = f"projects/{project.id}/templates/{tmpl_path}/{tmpl_id}" + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=f"http://localhost/api/v4/{path}", + json=tmpl_content, + ) + + template = getattr(project, tmpl_mgr).get(tmpl_id) + + assert isinstance(template, tmpl) + assert getattr(template, template._id_attr) == tmpl_id From 24af3049772aeda1f33984ec67cb6dce2d4dd9db Mon Sep 17 00:00:00 2001 From: Gerry Pratama Date: Fri, 6 Dec 2024 23:40:12 -0700 Subject: [PATCH 5/5] docs(templates): added section for project templates --- docs/gl_objects/templates.rst | 72 ++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/gl_objects/templates.rst b/docs/gl_objects/templates.rst index f939e5ff3..123c669ba 100644 --- a/docs/gl_objects/templates.rst +++ b/docs/gl_objects/templates.rst @@ -99,7 +99,7 @@ Reference + :class:`gitlab.v4.objects.DockerfileManager` + :attr:`gitlab.Gitlab.gitlabciymls` -* GitLab API: Not documented. +* GitLab API: https://docs.gitlab.com/ce/api/templates/dockerfiles.html Examples -------- @@ -112,3 +112,73 @@ Get a Dockerfile template:: dockerfile = gl.dockerfiles.get('Python') print(dockerfile.content) + +Project templates +========================= + +These templates are project-specific versions of the templates above, as +well as issue and merge request templates. + +Reference +--------- + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectLicenseTemplate` + + :class:`gitlab.v4.objects.ProjectLicenseTemplateManager` + + :attr:`gitlab.v4.objects.Project.license_templates` + + :class:`gitlab.v4.objects.ProjectGitignoreTemplate` + + :class:`gitlab.v4.objects.ProjectGitignoreTemplateManager` + + :attr:`gitlab.v4.objects.Project.gitignore_templates` + + :class:`gitlab.v4.objects.ProjectGitlabciymlTemplate` + + :class:`gitlab.v4.objects.ProjectGitlabciymlTemplateManager` + + :attr:`gitlab.v4.objects.Project.gitlabciyml_templates` + + :class:`gitlab.v4.objects.ProjectDockerfileTemplate` + + :class:`gitlab.v4.objects.ProjectDockerfileTemplateManager` + + :attr:`gitlab.v4.objects.Project.dockerfile_templates` + + :class:`gitlab.v4.objects.ProjectIssueTemplate` + + :class:`gitlab.v4.objects.ProjectIssueTemplateManager` + + :attr:`gitlab.v4.objects.Project.issue_templates` + + :class:`gitlab.v4.objects.ProjectMergeRequestTemplate` + + :class:`gitlab.v4.objects.ProjectMergeRequestTemplateManager` + + :attr:`gitlab.v4.objects.Project.merge_request_templates` + +* GitLab API: https://docs.gitlab.com/ce/api/project_templates.html + +Examples +-------- + +List known project templates:: + + license_templates = project.license_templates.list() + gitignore_templates = project.gitignore_templates.list() + gitlabciyml_templates = project.gitlabciyml_templates.list() + dockerfile_templates = project.dockerfile_templates.list() + issue_templates = project.issue_templates.list() + merge_request_templates = project.merge_request_templates.list() + +Get project templates:: + + license_template = project.license_templates.get('apache-2.0') + gitignore_template = project.gitignore_templates.get('Python') + gitlabciyml_template = project.gitlabciyml_templates.get('Pelican') + dockerfile_template = project.dockerfile_templates.get('Python') + issue_template = project.issue_templates.get('Default') + merge_request_template = project.merge_request_templates.get('Default') + + print(license_template.content) + print(gitignore_template.content) + print(gitlabciyml_template.content) + print(dockerfile_template.content) + print(issue_template.content) + print(merge_request_template.content) + +Create an issue or merge request using a description template:: + + issue = project.issues.create({'title': 'I have a bug', + 'description': issue_template.content}) + mr = project.mergerequests.create({'source_branch': 'cool_feature', + 'target_branch': 'main', + 'title': 'merge cool feature', + 'description': merge_request_template.content}) + \ No newline at end of file