Skip to content

Commit

Permalink
feat: implement secure files API
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamb authored and nejch committed Nov 10, 2022
1 parent fcd72fe commit d0a0348
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/api-objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ API examples
gl_objects/repositories
gl_objects/repository_tags
gl_objects/search
gl_objects/secure_files
gl_objects/settings
gl_objects/snippets
gl_objects/statistics
Expand Down
47 changes: 47 additions & 0 deletions docs/gl_objects/secure_files.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
############
Secure Files
############

secure files
============

References
----------

* v4 API:

+ :class:`gitlab.v4.objects.SecureFile`
+ :class:`gitlab.v4.objects.SecureFileManager`
+ :attr:`gitlab.v4.objects.Project.secure_files`

* GitLab API: https://docs.gitlab.com/ee/api/secure_files.html

Examples
--------

Get a project secure file::

secure_files = gl.projects.get(1, lazy=True).secure_files.get(1)
print(secure_files.name)

List project secure files::

secure_files = gl.projects.get(1, lazy=True).secure_files.list()
print(secure_files[0].name)

Create project secure file::

secure_file = gl.projects.get(1).secure_files.create({"name": "test", "file": "secure.txt"})

Download a project secure file::

content = secure_file.download()
print(content)
with open("/tmp/secure.txt", "wb") as f:
secure_file.download(streamed=True, action=f.write)

Remove a project secure file::

gl.projects.get(1).secure_files.delete(1)
# or
secure_file.delete()
1 change: 1 addition & 0 deletions gitlab/v4/objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from .releases import *
from .repositories import *
from .runners import *
from .secure_files import *
from .settings import *
from .sidekiq import *
from .snippets import *
Expand Down
2 changes: 2 additions & 0 deletions gitlab/v4/objects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
from .releases import ProjectReleaseManager # noqa: F401
from .repositories import RepositoryMixin
from .runners import ProjectRunnerManager # noqa: F401
from .secure_files import SecureFileManager # noqa: F401
from .snippets import ProjectSnippetManager # noqa: F401
from .statistics import ( # noqa: F401
ProjectAdditionalStatisticsManager,
Expand Down Expand Up @@ -209,6 +210,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
remote_mirrors: "ProjectRemoteMirrorManager"
repositories: ProjectRegistryRepositoryManager
runners: ProjectRunnerManager
secure_files: SecureFileManager
services: ProjectServiceManager
snippets: ProjectSnippetManager
storage: "ProjectStorageManager"
Expand Down
69 changes: 69 additions & 0 deletions gitlab/v4/objects/secure_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
GitLab API:
https://docs.gitlab.com/ee/api/secure_files.html
"""
from typing import Any, Callable, cast, Iterator, Optional, TYPE_CHECKING, Union

import requests

from gitlab import cli
from gitlab import exceptions as exc
from gitlab import utils
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin
from gitlab.types import FileAttribute, RequiredOptional

__all__ = ["SecureFile", "SecureFileManager"]


class SecureFile(ObjectDeleteMixin, RESTObject):
@cli.register_custom_action("SecureFile")
@exc.on_http_error(exc.GitlabGetError)
def download(
self,
streamed: bool = False,
action: Optional[Callable[[bytes], None]] = None,
chunk_size: int = 1024,
*,
iterator: bool = False,
**kwargs: Any,
) -> Optional[Union[bytes, Iterator[Any]]]:
"""Download the secure file.
Args:
streamed: If True the data will be processed by chunks of
`chunk_size` and each chunk is passed to `action` for
treatment
iterator: If True directly return the underlying response
iterator
action: Callable responsible of dealing with chunk of
data
chunk_size: Size of each chunk
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the artifacts could not be retrieved
Returns:
The artifacts if `streamed` is False, None otherwise."""
path = f"{self.manager.path}/{self.id}/download"
result = self.manager.gitlab.http_get(
path, streamed=streamed, raw=True, **kwargs
)
if TYPE_CHECKING:
assert isinstance(result, requests.Response)
return utils.response_content(
result, streamed, action, chunk_size, iterator=iterator
)


class SecureFileManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/secure_files"
_obj_cls = SecureFile
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(required=("name", "file"))
_types = {"file": FileAttribute}

def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> SecureFile:
return cast(SecureFile, super().get(id=id, lazy=lazy, **kwargs))
101 changes: 101 additions & 0 deletions tests/unit/objects/test_secure_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
GitLab API: https://docs.gitlab.com/ee/api/secure_files.html
"""

import pytest
import responses

from gitlab.v4.objects import SecureFile

secure_file_content = {
"id": 1,
"name": "myfile.jks",
"checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac",
"checksum_algorithm": "sha256",
"created_at": "2022-02-22T22:22:22.222Z",
"expires_at": None,
"metadata": None,
}


@pytest.fixture
def resp_list_secure_files():
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/secure_files",
json=[secure_file_content],
content_type="application/json",
status=200,
)
yield rsps


@pytest.fixture
def resp_create_secure_file():
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
rsps.add(
method=responses.POST,
url="http://localhost/api/v4/projects/1/secure_files",
json=secure_file_content,
content_type="application/json",
status=200,
)
yield rsps


@pytest.fixture
def resp_download_secure_file(binary_content):
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/secure_files/1",
json=secure_file_content,
content_type="application/json",
status=200,
)
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/secure_files/1/download",
body=binary_content,
content_type="application/octet-stream",
status=200,
)
yield rsps


@pytest.fixture
def resp_remove_secure_file(no_content):
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.DELETE,
url="http://localhost/api/v4/projects/1/secure_files/1",
json=no_content,
content_type="application/json",
status=204,
)
yield rsps


def test_list_secure_files(project, resp_list_secure_files):
secure_files = project.secure_files.list()
assert len(secure_files) == 1
assert secure_files[0].id == 1
assert secure_files[0].name == "myfile.jks"


def test_create_secure_file(project, resp_create_secure_file):
secure_files = project.secure_files.create({"name": "test", "file": "myfile.jks"})
assert secure_files.id == 1
assert secure_files.name == "myfile.jks"


def test_download_secure_file(project, binary_content, resp_download_secure_file):
secure_file = project.secure_files.get(1)
secure_content = secure_file.download()
assert isinstance(secure_file, SecureFile)
assert secure_content == binary_content


def test_remove_secure_file(project, resp_remove_secure_file):
project.secure_files.delete(1)

0 comments on commit d0a0348

Please sign in to comment.