GeezerVison is showing github.com — feat(api): add support for the Draft notes API (#2728) · gagallo7/python-gitlab@ebf9d82
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: docs/api-objects.rst +1Lines changed: 1 addition & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -19,6 +19,7 @@ API examples | 19 | 19 | gl_objects/deploy_tokens | 20 | 20 | gl_objects/deployments | 21 | 21 | gl_objects/discussions | | 22 | + gl_objects/draft_notes | 22 | 23 | gl_objects/environments | 23 | 24 | gl_objects/events | 24 | 25 | gl_objects/epics | |
Collapse file Copy file name to clipboard +58Lines changed: 58 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +.. _draft-notes: | | 2 | + | | 3 | +########### | | 4 | +Draft Notes | | 5 | +########### | | 6 | + | | 7 | +Draft notes are pending, unpublished comments on merge requests. | | 8 | +They can be either start a discussion, or be associated with an existing discussion as a reply. | | 9 | +They are viewable only by the author until they are published. | | 10 | + | | 11 | +Reference | | 12 | +--------- | | 13 | + | | 14 | +* v4 API: | | 15 | + | | 16 | + + :class:`gitlab.v4.objects.ProjectMergeRequestDraftNote` | | 17 | + + :class:`gitlab.v4.objects.ProjectMergeRequestDraftNoteManager` | | 18 | + + :attr:`gitlab.v4.objects.ProjectMergeRequest.draft_notes` | | 19 | + | | 20 | + | | 21 | +* GitLab API: https://docs.gitlab.com/ee/api/draft_notes.html | | 22 | + | | 23 | +Examples | | 24 | +-------- | | 25 | + | | 26 | +List all draft notes for a merge request:: | | 27 | + | | 28 | + draft_notes = merge_request.draft_notes.list() | | 29 | + | | 30 | +Get a draft note for a merge request by ID:: | | 31 | + | | 32 | + draft_note = merge_request.draft_notes.get(note_id) | | 33 | + | | 34 | +.. warning:: | | 35 | + | | 36 | + When creating or updating draft notes, you can provide a complex nested ``position`` argument as a dictionary. | | 37 | + Please consult the upstream API documentation linked above for the exact up-to-date attributes. | | 38 | + | | 39 | +Create a draft note for a merge request:: | | 40 | + | | 41 | + draft_note = merge_request.draft_notes.create({'note': 'note content'}) | | 42 | + | | 43 | +Update an existing draft note:: | | 44 | + | | 45 | + draft_note.note = 'updated note content' | | 46 | + draft_note.save() | | 47 | + | | 48 | +Delete an existing draft note:: | | 49 | + | | 50 | + draft_note.delete() | | 51 | + | | 52 | +Publish an existing draft note:: | | 53 | + | | 54 | + draft_note.publish() | | 55 | + | | 56 | +Publish all existing draft notes for a merge request in bulk:: | | 57 | + | | 58 | + merge_request.draft_notes.bulk_publish() |
Collapse file Copy file name to clipboardExpand all lines: gitlab/client.py +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1061,6 +1061,8 @@ def http_put( | 1061 | 1061 | raw=raw, | 1062 | 1062 | **kwargs, | 1063 | 1063 | ) | | 1064 | + if result.status_code in gitlab.const.NO_JSON_RESPONSE_CODES: | | 1065 | + return result | 1064 | 1066 | try: | 1065 | 1067 | json_result = result.json() | 1066 | 1068 | if TYPE_CHECKING: | |
Collapse file Copy file name to clipboardExpand all lines: gitlab/v4/objects/__init__.py +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 18 | 18 | from .deploy_tokens import * | 19 | 19 | from .deployments import * | 20 | 20 | from .discussions import * | | 21 | +from .draft_notes import * | 21 | 22 | from .environments import * | 22 | 23 | from .epics import * | 23 | 24 | from .events import * | |
Collapse file Copy file name to clipboard +43Lines changed: 43 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +from typing import Any, cast, Union | | 2 | + | | 3 | +from gitlab.base import RESTManager, RESTObject | | 4 | +from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin | | 5 | +from gitlab.types import RequiredOptional | | 6 | + | | 7 | +__all__ = [ | | 8 | + "ProjectMergeRequestDraftNote", | | 9 | + "ProjectMergeRequestDraftNoteManager", | | 10 | +] | | 11 | + | | 12 | + | | 13 | +class ProjectMergeRequestDraftNote(ObjectDeleteMixin, SaveMixin, RESTObject): | | 14 | + def publish(self, **kwargs: Any) -> None: | | 15 | + path = f"{self.manager.path}/{self.encoded_id}/publish" | | 16 | + self.manager.gitlab.http_put(path, **kwargs) | | 17 | + | | 18 | + | | 19 | +class ProjectMergeRequestDraftNoteManager(CRUDMixin, RESTManager): | | 20 | + _path = "/projects/{project_id}/merge_requests/{mr_iid}/draft_notes" | | 21 | + _obj_cls = ProjectMergeRequestDraftNote | | 22 | + _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} | | 23 | + _create_attrs = RequiredOptional( | | 24 | + required=("note",), | | 25 | + optional=( | | 26 | + "commit_id", | | 27 | + "in_reply_to_discussion_id", | | 28 | + "position", | | 29 | + "resolve_discussion", | | 30 | + ), | | 31 | + ) | | 32 | + _update_attrs = RequiredOptional(optional=("position",)) | | 33 | + | | 34 | + def get( | | 35 | + self, id: Union[str, int], lazy: bool = False, **kwargs: Any | | 36 | + ) -> ProjectMergeRequestDraftNote: | | 37 | + return cast( | | 38 | + ProjectMergeRequestDraftNote, super().get(id=id, lazy=lazy, **kwargs) | | 39 | + ) | | 40 | + | | 41 | + def bulk_publish(self, **kwargs: Any) -> None: | | 42 | + path = f"{self.path}/bulk_publish" | | 43 | + self.gitlab.http_post(path, **kwargs) |
Collapse file Copy file name to clipboardExpand all lines: gitlab/v4/objects/merge_requests.py +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 28 | 28 | from .award_emojis import ProjectMergeRequestAwardEmojiManager # noqa: F401 | 29 | 29 | from .commits import ProjectCommit, ProjectCommitManager | 30 | 30 | from .discussions import ProjectMergeRequestDiscussionManager # noqa: F401 | | 31 | +from .draft_notes import ProjectMergeRequestDraftNoteManager | 31 | 32 | from .events import ( # noqa: F401 | 32 | 33 | ProjectMergeRequestResourceLabelEventManager, | 33 | 34 | ProjectMergeRequestResourceMilestoneEventManager, | @@ -157,6 +158,7 @@ class ProjectMergeRequest( | 157 | 158 | awardemojis: ProjectMergeRequestAwardEmojiManager | 158 | 159 | diffs: "ProjectMergeRequestDiffManager" | 159 | 160 | discussions: ProjectMergeRequestDiscussionManager | | 161 | + draft_notes: ProjectMergeRequestDraftNoteManager | 160 | 162 | notes: ProjectMergeRequestNoteManager | 161 | 163 | pipelines: ProjectMergeRequestPipelineManager | 162 | 164 | resourcelabelevents: ProjectMergeRequestResourceLabelEventManager | |
Collapse file Copy file name to clipboard +175Lines changed: 175 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +""" | | 2 | +GitLab API: https://docs.gitlab.com/ee/api/draft_notes.html | | 3 | +""" | | 4 | +from copy import deepcopy | | 5 | + | | 6 | +import pytest | | 7 | +import responses | | 8 | + | | 9 | +from gitlab.v4.objects import ProjectMergeRequestDraftNote | | 10 | + | | 11 | +draft_note_content = { | | 12 | + "id": 1, | | 13 | + "author_id": 23, | | 14 | + "merge_request_id": 1, | | 15 | + "resolve_discussion": False, | | 16 | + "discussion_id": None, | | 17 | + "note": "Example title", | | 18 | + "commit_id": None, | | 19 | + "line_code": None, | | 20 | + "position": { | | 21 | + "base_sha": None, | | 22 | + "start_sha": None, | | 23 | + "head_sha": None, | | 24 | + "old_path": None, | | 25 | + "new_path": None, | | 26 | + "position_type": "text", | | 27 | + "old_line": None, | | 28 | + "new_line": None, | | 29 | + "line_range": None, | | 30 | + }, | | 31 | +} | | 32 | + | | 33 | + | | 34 | +@pytest.fixture() | | 35 | +def resp_list_merge_request_draft_notes(): | | 36 | + with responses.RequestsMock() as rsps: | | 37 | + rsps.add( | | 38 | + method=responses.GET, | | 39 | + url="http://localhost/api/v4/projects/1/merge_requests/1/draft_notes", | | 40 | + json=[draft_note_content], | | 41 | + content_type="application/json", | | 42 | + status=200, | | 43 | + ) | | 44 | + yield rsps | | 45 | + | | 46 | + | | 47 | +@pytest.fixture() | | 48 | +def resp_get_merge_request_draft_note(): | | 49 | + with responses.RequestsMock() as rsps: | | 50 | + rsps.add( | | 51 | + method=responses.GET, | | 52 | + url="http://localhost/api/v4/projects/1/merge_requests/1/draft_notes/1", | | 53 | + json=draft_note_content, | | 54 | + content_type="application/json", | | 55 | + status=200, | | 56 | + ) | | 57 | + yield rsps | | 58 | + | | 59 | + | | 60 | +@pytest.fixture() | | 61 | +def resp_create_merge_request_draft_note(): | | 62 | + with responses.RequestsMock() as rsps: | | 63 | + rsps.add( | | 64 | + method=responses.POST, | | 65 | + url="http://localhost/api/v4/projects/1/merge_requests/1/draft_notes", | | 66 | + json=draft_note_content, | | 67 | + content_type="application/json", | | 68 | + status=201, | | 69 | + ) | | 70 | + yield rsps | | 71 | + | | 72 | + | | 73 | +@pytest.fixture() | | 74 | +def resp_update_merge_request_draft_note(): | | 75 | + updated_content = deepcopy(draft_note_content) | | 76 | + updated_content["note"] = "New title" | | 77 | + | | 78 | + with responses.RequestsMock() as rsps: | | 79 | + rsps.add( | | 80 | + method=responses.PUT, | | 81 | + url="http://localhost/api/v4/projects/1/merge_requests/1/draft_notes/1", | | 82 | + json=updated_content, | | 83 | + content_type="application/json", | | 84 | + status=201, | | 85 | + ) | | 86 | + yield rsps | | 87 | + | | 88 | + | | 89 | +@pytest.fixture() | | 90 | +def resp_delete_merge_request_draft_note(): | | 91 | + with responses.RequestsMock() as rsps: | | 92 | + rsps.add( | | 93 | + method=responses.DELETE, | | 94 | + url="http://localhost/api/v4/projects/1/merge_requests/1/draft_notes/1", | | 95 | + json=draft_note_content, | | 96 | + content_type="application/json", | | 97 | + status=201, | | 98 | + ) | | 99 | + yield rsps | | 100 | + | | 101 | + | | 102 | +@pytest.fixture() | | 103 | +def resp_publish_merge_request_draft_note(): | | 104 | + with responses.RequestsMock() as rsps: | | 105 | + rsps.add( | | 106 | + method=responses.PUT, | | 107 | + url="http://localhost/api/v4/projects/1/merge_requests/1/draft_notes/1/publish", | | 108 | + status=204, | | 109 | + ) | | 110 | + yield rsps | | 111 | + | | 112 | + | | 113 | +@pytest.fixture() | | 114 | +def resp_bulk_publish_merge_request_draft_notes(): | | 115 | + with responses.RequestsMock() as rsps: | | 116 | + rsps.add( | | 117 | + method=responses.POST, | | 118 | + url="http://localhost/api/v4/projects/1/merge_requests/1/draft_notes/bulk_publish", | | 119 | + status=204, | | 120 | + ) | | 121 | + yield rsps | | 122 | + | | 123 | + | | 124 | +def test_list_merge_requests_draft_notes( | | 125 | + project_merge_request, resp_list_merge_request_draft_notes | | 126 | +): | | 127 | + draft_notes = project_merge_request.draft_notes.list() | | 128 | + assert len(draft_notes) == 1 | | 129 | + assert isinstance(draft_notes[0], ProjectMergeRequestDraftNote) | | 130 | + assert draft_notes[0].note == draft_note_content["note"] | | 131 | + | | 132 | + | | 133 | +def test_get_merge_requests_draft_note( | | 134 | + project_merge_request, resp_get_merge_request_draft_note | | 135 | +): | | 136 | + draft_note = project_merge_request.draft_notes.get(1) | | 137 | + assert isinstance(draft_note, ProjectMergeRequestDraftNote) | | 138 | + assert draft_note.note == draft_note_content["note"] | | 139 | + | | 140 | + | | 141 | +def test_create_merge_requests_draft_note( | | 142 | + project_merge_request, resp_create_merge_request_draft_note | | 143 | +): | | 144 | + draft_note = project_merge_request.draft_notes.create({"note": "Example title"}) | | 145 | + assert isinstance(draft_note, ProjectMergeRequestDraftNote) | | 146 | + assert draft_note.note == draft_note_content["note"] | | 147 | + | | 148 | + | | 149 | +def test_update_merge_requests_draft_note( | | 150 | + project_merge_request, resp_update_merge_request_draft_note | | 151 | +): | | 152 | + draft_note = project_merge_request.draft_notes.get(1, lazy=True) | | 153 | + draft_note.note = "New title" | | 154 | + draft_note.save() | | 155 | + assert draft_note.note == "New title" | | 156 | + | | 157 | + | | 158 | +def test_delete_merge_requests_draft_note( | | 159 | + project_merge_request, resp_delete_merge_request_draft_note | | 160 | +): | | 161 | + draft_note = project_merge_request.draft_notes.get(1, lazy=True) | | 162 | + draft_note.delete() | | 163 | + | | 164 | + | | 165 | +def test_publish_merge_requests_draft_note( | | 166 | + project_merge_request, resp_publish_merge_request_draft_note | | 167 | +): | | 168 | + draft_note = project_merge_request.draft_notes.get(1, lazy=True) | | 169 | + draft_note.publish() | | 170 | + | | 171 | + | | 172 | +def test_bulk_publish_merge_requests_draft_notes( | | 173 | + project_merge_request, resp_bulk_publish_merge_request_draft_notes | | 174 | +): | | 175 | + project_merge_request.draft_notes.bulk_publish() |
Collapse file Copy file name to clipboardExpand all lines: tests/unit/test_gitlab_http_methods.py +15Lines changed: 15 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -766,6 +766,21 @@ def test_put_request_404(gl): | 766 | 766 | assert responses.assert_call_count(url, 1) is True | 767 | 767 |
| 768 | 768 |
| | 769 | +@responses.activate | | 770 | +def test_put_request_204(gl): | | 771 | + url = "http://localhost/api/v4/projects" | | 772 | + responses.add( | | 773 | + method=responses.PUT, | | 774 | + url=url, | | 775 | + status=204, | | 776 | + match=helpers.MATCH_EMPTY_QUERY_PARAMS, | | 777 | + ) | | 778 | + | | 779 | + result = gl.http_put("/projects") | | 780 | + assert isinstance(result, requests.Response) | | 781 | + assert responses.assert_call_count(url, 1) is True | | 782 | + | | 783 | + | 769 | 784 | @responses.activate | 770 | 785 | def test_put_request_invalid_data(gl): | 771 | 786 | url = "http://localhost/api/v4/projects" | |
|