Skip to content

Commit 7b864b8

Browse files
feat(api): add support for wiki attachments (#2722)
Added UploadMixin in mixin module Added UploadMixin dependency for Project, ProjectWiki, GroupWiki Added api tests for wiki upload Added unit test for mixin Added docs sections to wikis.rst
1 parent f4ce867 commit 7b864b8

File tree

6 files changed

+262
-59
lines changed

6 files changed

+262
-59
lines changed

docs/gl_objects/wikis.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,41 @@ Update a wiki page::
5454
Delete a wiki page::
5555

5656
page.delete()
57+
58+
59+
File uploads
60+
============
61+
62+
Reference
63+
---------
64+
65+
* v4 API:
66+
67+
+ :attr:`gitlab.v4.objects.ProjectWiki.upload`
68+
+ :attr:`gitlab.v4.objects.GrouptWiki.upload`
69+
70+
71+
* Gitlab API for Projects: https://docs.gitlab.com/ee/api/wikis.html#upload-an-attachment-to-the-wiki-repository
72+
* Gitlab API for Groups: https://docs.gitlab.com/ee/api/group_wikis.html#upload-an-attachment-to-the-wiki-repository
73+
74+
Examples
75+
--------
76+
77+
Upload a file into a project wiki using a filesystem path::
78+
79+
page = project.wikis.get(page_slug)
80+
page.upload("filename.txt", filepath="/some/path/filename.txt")
81+
82+
Upload a file into a project wiki with raw data::
83+
84+
page.upload("filename.txt", filedata="Raw data")
85+
86+
Upload a file into a group wiki using a filesystem path::
87+
88+
page = group.wikis.get(page_slug)
89+
page.upload("filename.txt", filepath="/some/path/filename.txt")
90+
91+
Upload a file into a group wiki using raw data::
92+
93+
page.upload("filename.txt", filedata="Raw data")
94+

gitlab/mixins.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,73 @@ def promote(self, **kwargs: Any) -> Dict[str, Any]:
944944
if TYPE_CHECKING:
945945
assert not isinstance(result, requests.Response)
946946
return result
947+
948+
949+
class UploadMixin(_RestObjectBase):
950+
_id_attr: Optional[str]
951+
_attrs: Dict[str, Any]
952+
_module: ModuleType
953+
_parent_attrs: Dict[str, Any]
954+
_updated_attrs: Dict[str, Any]
955+
_upload_path: str
956+
manager: base.RESTManager
957+
958+
def _get_upload_path(self) -> str:
959+
"""Formats _upload_path with object attributes.
960+
961+
Returns:
962+
The upload path
963+
"""
964+
if TYPE_CHECKING:
965+
assert isinstance(self._upload_path, str)
966+
data = self.attributes
967+
return self._upload_path.format(**data)
968+
969+
@cli.register_custom_action(("Project", "ProjectWiki"), ("filename", "filepath"))
970+
@exc.on_http_error(exc.GitlabUploadError)
971+
def upload(
972+
self,
973+
filename: str,
974+
filedata: Optional[bytes] = None,
975+
filepath: Optional[str] = None,
976+
**kwargs: Any,
977+
) -> Dict[str, Any]:
978+
"""Upload the specified file.
979+
980+
.. note::
981+
982+
Either ``filedata`` or ``filepath`` *MUST* be specified.
983+
984+
Args:
985+
filename: The name of the file being uploaded
986+
filedata: The raw data of the file being uploaded
987+
filepath: The path to a local file to upload (optional)
988+
989+
Raises:
990+
GitlabAuthenticationError: If authentication is not correct
991+
GitlabUploadError: If the file upload fails
992+
GitlabUploadError: If ``filedata`` and ``filepath`` are not
993+
specified
994+
GitlabUploadError: If both ``filedata`` and ``filepath`` are
995+
specified
996+
997+
Returns:
998+
A ``dict`` with info on the uploaded file
999+
"""
1000+
if filepath is None and filedata is None:
1001+
raise exc.GitlabUploadError("No file contents or path specified")
1002+
1003+
if filedata is not None and filepath is not None:
1004+
raise exc.GitlabUploadError("File contents and file path specified")
1005+
1006+
if filepath is not None:
1007+
with open(filepath, "rb") as f:
1008+
filedata = f.read()
1009+
1010+
file_info = {"file": (filename, filedata)}
1011+
path = self._get_upload_path()
1012+
server_data = self.manager.gitlab.http_post(path, files=file_info, **kwargs)
1013+
1014+
if TYPE_CHECKING:
1015+
assert isinstance(server_data, dict)
1016+
return server_data

gitlab/v4/objects/projects.py

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
RefreshMixin,
3131
SaveMixin,
3232
UpdateMixin,
33+
UploadMixin,
3334
)
3435
from gitlab.types import RequiredOptional
3536

@@ -158,8 +159,11 @@ class ProjectGroupManager(ListMixin, RESTManager):
158159
_types = {"skip_groups": types.ArrayAttribute}
159160

160161

161-
class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTObject):
162+
class Project(
163+
RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, UploadMixin, RESTObject
164+
):
162165
_repr_attr = "path_with_namespace"
166+
_upload_path = "/projects/{id}/uploads"
163167

164168
access_tokens: ProjectAccessTokenManager
165169
accessrequests: ProjectAccessRequestManager
@@ -437,59 +441,6 @@ def housekeeping(self, **kwargs: Any) -> None:
437441
path = f"/projects/{self.encoded_id}/housekeeping"
438442
self.manager.gitlab.http_post(path, **kwargs)
439443

440-
# see #56 - add file attachment features
441-
@cli.register_custom_action("Project", ("filename", "filepath"))
442-
@exc.on_http_error(exc.GitlabUploadError)
443-
def upload(
444-
self,
445-
filename: str,
446-
filedata: Optional[bytes] = None,
447-
filepath: Optional[str] = None,
448-
**kwargs: Any,
449-
) -> Dict[str, Any]:
450-
"""Upload the specified file into the project.
451-
452-
.. note::
453-
454-
Either ``filedata`` or ``filepath`` *MUST* be specified.
455-
456-
Args:
457-
filename: The name of the file being uploaded
458-
filedata: The raw data of the file being uploaded
459-
filepath: The path to a local file to upload (optional)
460-
461-
Raises:
462-
GitlabConnectionError: If the server cannot be reached
463-
GitlabUploadError: If the file upload fails
464-
GitlabUploadError: If ``filedata`` and ``filepath`` are not
465-
specified
466-
GitlabUploadError: If both ``filedata`` and ``filepath`` are
467-
specified
468-
469-
Returns:
470-
A ``dict`` with the keys:
471-
* ``alt`` - The alternate text for the upload
472-
* ``url`` - The direct url to the uploaded file
473-
* ``markdown`` - Markdown for the uploaded file
474-
"""
475-
if filepath is None and filedata is None:
476-
raise exc.GitlabUploadError("No file contents or path specified")
477-
478-
if filedata is not None and filepath is not None:
479-
raise exc.GitlabUploadError("File contents and file path specified")
480-
481-
if filepath is not None:
482-
with open(filepath, "rb") as f:
483-
filedata = f.read()
484-
485-
url = f"/projects/{self.encoded_id}/uploads"
486-
file_info = {"file": (filename, filedata)}
487-
data = self.manager.gitlab.http_post(url, files=file_info, **kwargs)
488-
489-
if TYPE_CHECKING:
490-
assert isinstance(data, dict)
491-
return {"alt": data["alt"], "url": data["url"], "markdown": data["markdown"]}
492-
493444
@cli.register_custom_action("Project")
494445
@exc.on_http_error(exc.GitlabRestoreError)
495446
def restore(self, **kwargs: Any) -> None:

gitlab/v4/objects/wikis.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, cast, Union
22

33
from gitlab.base import RESTManager, RESTObject
4-
from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
4+
from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin, UploadMixin
55
from gitlab.types import RequiredOptional
66

77
__all__ = [
@@ -12,9 +12,10 @@
1212
]
1313

1414

15-
class ProjectWiki(SaveMixin, ObjectDeleteMixin, RESTObject):
15+
class ProjectWiki(SaveMixin, ObjectDeleteMixin, UploadMixin, RESTObject):
1616
_id_attr = "slug"
1717
_repr_attr = "slug"
18+
_upload_path = "/projects/{project_id}/wikis/attachments"
1819

1920

2021
class ProjectWikiManager(CRUDMixin, RESTManager):
@@ -33,9 +34,10 @@ def get(
3334
return cast(ProjectWiki, super().get(id=id, lazy=lazy, **kwargs))
3435

3536

36-
class GroupWiki(SaveMixin, ObjectDeleteMixin, RESTObject):
37+
class GroupWiki(SaveMixin, ObjectDeleteMixin, UploadMixin, RESTObject):
3738
_id_attr = "slug"
3839
_repr_attr = "slug"
40+
_upload_path = "/groups/{group_id}/wikis/attachments"
3941

4042

4143
class GroupWikiManager(CRUDMixin, RESTManager):

tests/functional/api/test_wikis.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,59 @@
44
"""
55

66

7-
def test_wikis(project):
7+
def test_project_wikis(project):
88
page = project.wikis.create({"title": "title/subtitle", "content": "test content"})
99
page.content = "update content"
1010
page.title = "subtitle"
1111

1212
page.save()
1313

1414
page.delete()
15+
16+
17+
def test_project_wiki_file_upload(project):
18+
page = project.wikis.create(
19+
{"title": "title/subtitle", "content": "test page content"}
20+
)
21+
filename = "test.txt"
22+
file_contents = "testing contents"
23+
24+
uploaded_file = page.upload(filename, file_contents)
25+
26+
link = uploaded_file["link"]
27+
file_name = uploaded_file["file_name"]
28+
file_path = uploaded_file["file_path"]
29+
assert file_name == filename
30+
assert file_path.startswith("uploads/")
31+
assert file_path.endswith(f"/{filename}")
32+
assert link["url"] == file_path
33+
assert link["markdown"] == f"[{file_name}]({file_path})"
34+
35+
36+
def test_group_wikis(group):
37+
page = group.wikis.create({"title": "title/subtitle", "content": "test content"})
38+
page.content = "update content"
39+
page.title = "subtitle"
40+
41+
page.save()
42+
43+
page.delete()
44+
45+
46+
def test_group_wiki_file_upload(group):
47+
page = group.wikis.create(
48+
{"title": "title/subtitle", "content": "test page content"}
49+
)
50+
filename = "test.txt"
51+
file_contents = "testing contents"
52+
53+
uploaded_file = page.upload(filename, file_contents)
54+
55+
link = uploaded_file["link"]
56+
file_name = uploaded_file["file_name"]
57+
file_path = uploaded_file["file_path"]
58+
assert file_name == filename
59+
assert file_path.startswith("uploads/")
60+
assert file_path.endswith(f"/{filename}")
61+
assert link["url"] == file_path
62+
assert link["markdown"] == f"[{file_name}]({file_path})"

0 commit comments

Comments
 (0)