Skip to content

Commit f14a2cc

Browse files
committed
feat(api): add support for wiki attachments
UploadMixin added, inherits from RESTObject; UploadMixin used in projects.py and wikis.py to upload files; Test for upload to wiki api added in test_wikis
1 parent d0546e0 commit f14a2cc

File tree

4 files changed

+102
-56
lines changed

4 files changed

+102
-56
lines changed

gitlab/mixins.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,78 @@ 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_string with object attributes.
960+
961+
Returns:
962+
The upload path
963+
"""
964+
if not self._upload_path:
965+
raise exc.GitlabUploadError(
966+
f"No upload path set for {self.__class__.__name__}"
967+
)
968+
if TYPE_CHECKING:
969+
assert isinstance(self._upload_path, str)
970+
data = self.attributes
971+
return self._upload_path.format(**data)
972+
973+
@cli.register_custom_action(("Project", "ProjectWiki"), ("filename", "filepath"))
974+
@exc.on_http_error(exc.GitlabUploadError)
975+
def upload(
976+
self,
977+
filename: str,
978+
filedata: Optional[bytes] = None,
979+
filepath: Optional[str] = None,
980+
**kwargs: Any,
981+
) -> Dict[str, Any]:
982+
"""Upload the specified file.
983+
984+
.. note::
985+
986+
Either ``filedata`` or ``filepath`` *MUST* be specified.
987+
988+
Args:
989+
path: api endpoint where file is to be posted
990+
filename: The name of the file being uploaded
991+
filedata: The raw data of the file being uploaded
992+
filepath: The path to a local file to upload (optional)
993+
994+
Raises:
995+
GitlabAuthenticationError: If authentication is not correct
996+
GitlabUploadError: If the file upload fails
997+
GitlabUploadError: If ``filedata`` and ``filepath`` are not
998+
specified
999+
GitlabUploadError: If both ``filedata`` and ``filepath`` are
1000+
specified
1001+
1002+
Returns:
1003+
A ``dict`` with info on the uploaded file
1004+
"""
1005+
if filepath is None and filedata is None:
1006+
raise exc.GitlabUploadError("No file contents or path specified")
1007+
1008+
if filedata is not None and filepath is not None:
1009+
raise exc.GitlabUploadError("File contents and file path specified")
1010+
1011+
if filepath is not None:
1012+
with open(filepath, "rb") as f:
1013+
filedata = f.read()
1014+
1015+
file_info = {"file": (filename, filedata)}
1016+
path = self._get_upload_path()
1017+
server_data = self.manager.gitlab.http_post(path, files=file_info, **kwargs)
1018+
1019+
if TYPE_CHECKING:
1020+
assert isinstance(server_data, dict)
1021+
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: 3 additions & 2 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):

tests/functional/api/test_wikis.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,22 @@ def test_wikis(project):
1212
page.save()
1313

1414
page.delete()
15+
16+
17+
def test_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})"

0 commit comments

Comments
 (0)