Skip to content

Commit d0a0348

Browse files
benjambnejch
authored andcommitted
feat: implement secure files API
1 parent fcd72fe commit d0a0348

File tree

6 files changed

+221
-0
lines changed

6 files changed

+221
-0
lines changed

docs/api-objects.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ API examples
5252
gl_objects/repositories
5353
gl_objects/repository_tags
5454
gl_objects/search
55+
gl_objects/secure_files
5556
gl_objects/settings
5657
gl_objects/snippets
5758
gl_objects/statistics

docs/gl_objects/secure_files.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
############
2+
Secure Files
3+
############
4+
5+
secure files
6+
============
7+
8+
References
9+
----------
10+
11+
* v4 API:
12+
13+
+ :class:`gitlab.v4.objects.SecureFile`
14+
+ :class:`gitlab.v4.objects.SecureFileManager`
15+
+ :attr:`gitlab.v4.objects.Project.secure_files`
16+
17+
* GitLab API: https://docs.gitlab.com/ee/api/secure_files.html
18+
19+
Examples
20+
--------
21+
22+
Get a project secure file::
23+
24+
secure_files = gl.projects.get(1, lazy=True).secure_files.get(1)
25+
print(secure_files.name)
26+
27+
List project secure files::
28+
29+
secure_files = gl.projects.get(1, lazy=True).secure_files.list()
30+
print(secure_files[0].name)
31+
32+
Create project secure file::
33+
34+
secure_file = gl.projects.get(1).secure_files.create({"name": "test", "file": "secure.txt"})
35+
36+
Download a project secure file::
37+
38+
content = secure_file.download()
39+
print(content)
40+
with open("/tmp/secure.txt", "wb") as f:
41+
secure_file.download(streamed=True, action=f.write)
42+
43+
Remove a project secure file::
44+
45+
gl.projects.get(1).secure_files.delete(1)
46+
# or
47+
secure_file.delete()

gitlab/v4/objects/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from .releases import *
5454
from .repositories import *
5555
from .runners import *
56+
from .secure_files import *
5657
from .settings import *
5758
from .sidekiq import *
5859
from .snippets import *

gitlab/v4/objects/projects.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
from .releases import ProjectReleaseManager # noqa: F401
8383
from .repositories import RepositoryMixin
8484
from .runners import ProjectRunnerManager # noqa: F401
85+
from .secure_files import SecureFileManager # noqa: F401
8586
from .snippets import ProjectSnippetManager # noqa: F401
8687
from .statistics import ( # noqa: F401
8788
ProjectAdditionalStatisticsManager,
@@ -209,6 +210,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
209210
remote_mirrors: "ProjectRemoteMirrorManager"
210211
repositories: ProjectRegistryRepositoryManager
211212
runners: ProjectRunnerManager
213+
secure_files: SecureFileManager
212214
services: ProjectServiceManager
213215
snippets: ProjectSnippetManager
214216
storage: "ProjectStorageManager"

gitlab/v4/objects/secure_files.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
GitLab API:
3+
https://docs.gitlab.com/ee/api/secure_files.html
4+
"""
5+
from typing import Any, Callable, cast, Iterator, Optional, TYPE_CHECKING, Union
6+
7+
import requests
8+
9+
from gitlab import cli
10+
from gitlab import exceptions as exc
11+
from gitlab import utils
12+
from gitlab.base import RESTManager, RESTObject
13+
from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin
14+
from gitlab.types import FileAttribute, RequiredOptional
15+
16+
__all__ = ["SecureFile", "SecureFileManager"]
17+
18+
19+
class SecureFile(ObjectDeleteMixin, RESTObject):
20+
@cli.register_custom_action("SecureFile")
21+
@exc.on_http_error(exc.GitlabGetError)
22+
def download(
23+
self,
24+
streamed: bool = False,
25+
action: Optional[Callable[[bytes], None]] = None,
26+
chunk_size: int = 1024,
27+
*,
28+
iterator: bool = False,
29+
**kwargs: Any,
30+
) -> Optional[Union[bytes, Iterator[Any]]]:
31+
"""Download the secure file.
32+
33+
Args:
34+
streamed: If True the data will be processed by chunks of
35+
`chunk_size` and each chunk is passed to `action` for
36+
treatment
37+
iterator: If True directly return the underlying response
38+
iterator
39+
action: Callable responsible of dealing with chunk of
40+
data
41+
chunk_size: Size of each chunk
42+
**kwargs: Extra options to send to the server (e.g. sudo)
43+
44+
Raises:
45+
GitlabAuthenticationError: If authentication is not correct
46+
GitlabGetError: If the artifacts could not be retrieved
47+
48+
Returns:
49+
The artifacts if `streamed` is False, None otherwise."""
50+
path = f"{self.manager.path}/{self.id}/download"
51+
result = self.manager.gitlab.http_get(
52+
path, streamed=streamed, raw=True, **kwargs
53+
)
54+
if TYPE_CHECKING:
55+
assert isinstance(result, requests.Response)
56+
return utils.response_content(
57+
result, streamed, action, chunk_size, iterator=iterator
58+
)
59+
60+
61+
class SecureFileManager(NoUpdateMixin, RESTManager):
62+
_path = "/projects/{project_id}/secure_files"
63+
_obj_cls = SecureFile
64+
_from_parent_attrs = {"project_id": "id"}
65+
_create_attrs = RequiredOptional(required=("name", "file"))
66+
_types = {"file": FileAttribute}
67+
68+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> SecureFile:
69+
return cast(SecureFile, super().get(id=id, lazy=lazy, **kwargs))
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
GitLab API: https://docs.gitlab.com/ee/api/secure_files.html
3+
"""
4+
5+
import pytest
6+
import responses
7+
8+
from gitlab.v4.objects import SecureFile
9+
10+
secure_file_content = {
11+
"id": 1,
12+
"name": "myfile.jks",
13+
"checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac",
14+
"checksum_algorithm": "sha256",
15+
"created_at": "2022-02-22T22:22:22.222Z",
16+
"expires_at": None,
17+
"metadata": None,
18+
}
19+
20+
21+
@pytest.fixture
22+
def resp_list_secure_files():
23+
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
24+
rsps.add(
25+
method=responses.GET,
26+
url="http://localhost/api/v4/projects/1/secure_files",
27+
json=[secure_file_content],
28+
content_type="application/json",
29+
status=200,
30+
)
31+
yield rsps
32+
33+
34+
@pytest.fixture
35+
def resp_create_secure_file():
36+
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
37+
rsps.add(
38+
method=responses.POST,
39+
url="http://localhost/api/v4/projects/1/secure_files",
40+
json=secure_file_content,
41+
content_type="application/json",
42+
status=200,
43+
)
44+
yield rsps
45+
46+
47+
@pytest.fixture
48+
def resp_download_secure_file(binary_content):
49+
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
50+
rsps.add(
51+
method=responses.GET,
52+
url="http://localhost/api/v4/projects/1/secure_files/1",
53+
json=secure_file_content,
54+
content_type="application/json",
55+
status=200,
56+
)
57+
rsps.add(
58+
method=responses.GET,
59+
url="http://localhost/api/v4/projects/1/secure_files/1/download",
60+
body=binary_content,
61+
content_type="application/octet-stream",
62+
status=200,
63+
)
64+
yield rsps
65+
66+
67+
@pytest.fixture
68+
def resp_remove_secure_file(no_content):
69+
with responses.RequestsMock() as rsps:
70+
rsps.add(
71+
method=responses.DELETE,
72+
url="http://localhost/api/v4/projects/1/secure_files/1",
73+
json=no_content,
74+
content_type="application/json",
75+
status=204,
76+
)
77+
yield rsps
78+
79+
80+
def test_list_secure_files(project, resp_list_secure_files):
81+
secure_files = project.secure_files.list()
82+
assert len(secure_files) == 1
83+
assert secure_files[0].id == 1
84+
assert secure_files[0].name == "myfile.jks"
85+
86+
87+
def test_create_secure_file(project, resp_create_secure_file):
88+
secure_files = project.secure_files.create({"name": "test", "file": "myfile.jks"})
89+
assert secure_files.id == 1
90+
assert secure_files.name == "myfile.jks"
91+
92+
93+
def test_download_secure_file(project, binary_content, resp_download_secure_file):
94+
secure_file = project.secure_files.get(1)
95+
secure_content = secure_file.download()
96+
assert isinstance(secure_file, SecureFile)
97+
assert secure_content == binary_content
98+
99+
100+
def test_remove_secure_file(project, resp_remove_secure_file):
101+
project.secure_files.delete(1)

0 commit comments

Comments
 (0)