Skip to content

feat: Allow uploading packages from files and bytes #2633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions gitlab/_backends/requests_backend.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import dataclasses
from typing import Any, Dict, Optional, TYPE_CHECKING, Union
from typing import Any, BinaryIO, Dict, Optional, TYPE_CHECKING, Union

import requests
from requests import PreparedRequest
Expand Down Expand Up @@ -94,7 +94,7 @@ def client(self) -> requests.Session:
@staticmethod
def prepare_send_data(
files: Optional[Dict[str, Any]] = None,
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
raw: bool = False,
) -> SendData:
if files:
Expand All @@ -121,6 +121,9 @@ def prepare_send_data(
if raw and post_data:
return SendData(data=post_data, content_type="application/octet-stream")

if TYPE_CHECKING:
assert not isinstance(post_data, BinaryIO)

return SendData(json=post_data, content_type="application/json")

def http_request(
Expand Down
17 changes: 14 additions & 3 deletions gitlab/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
import os
import re
import time
from typing import Any, cast, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union
from typing import (
Any,
BinaryIO,
cast,
Dict,
List,
Optional,
Tuple,
Type,
TYPE_CHECKING,
Union,
)
from urllib import parse

import requests
Expand Down Expand Up @@ -612,7 +623,7 @@ def http_request(
verb: str,
path: str,
query_data: Optional[Dict[str, Any]] = None,
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
raw: bool = False,
streamed: bool = False,
files: Optional[Dict[str, Any]] = None,
Expand Down Expand Up @@ -993,7 +1004,7 @@ def http_put(
self,
path: str,
query_data: Optional[Dict[str, Any]] = None,
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
raw: bool = False,
files: Optional[Dict[str, Any]] = None,
**kwargs: Any,
Expand Down
41 changes: 33 additions & 8 deletions gitlab/v4/objects/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
"""

from pathlib import Path
from typing import Any, Callable, cast, Iterator, Optional, TYPE_CHECKING, Union
from typing import (
Any,
BinaryIO,
Callable,
cast,
Iterator,
Optional,
TYPE_CHECKING,
Union,
)

import requests

Expand Down Expand Up @@ -46,8 +55,9 @@ def upload(
package_name: str,
package_version: str,
file_name: str,
path: Union[str, Path],
path: Optional[Union[str, Path]] = None,
select: Optional[str] = None,
data: Optional[Union[bytes, BinaryIO]] = None,
**kwargs: Any,
) -> GenericPackage:
"""Upload a file as a generic package.
Expand All @@ -64,19 +74,34 @@ def upload(
Raises:
GitlabConnectionError: If the server cannot be reached
GitlabUploadError: If the file upload fails
GitlabUploadError: If ``filepath`` cannot be read
GitlabUploadError: If ``path`` cannot be read
GitlabUploadError: If both ``path`` and ``data`` are passed

Returns:
An object storing the metadata of the uploaded package.

https://docs.gitlab.com/ee/user/packages/generic_packages/
"""

try:
with open(path, "rb") as f:
file_data = f.read()
except OSError as e:
raise exc.GitlabUploadError(f"Failed to read package file {path}") from e
if path is None and data is None:
raise exc.GitlabUploadError("No file contents or path specified")

if path is not None and data is not None:
raise exc.GitlabUploadError("File contents and file path specified")

file_data: Optional[Union[bytes, BinaryIO]] = data

if not file_data:
if TYPE_CHECKING:
assert path is not None

try:
with open(path, "rb") as f:
file_data = f.read()
except OSError as e:
raise exc.GitlabUploadError(
f"Failed to read package file {path}"
) from e

url = f"{self._computed_path}/{package_name}/{package_version}/{file_name}"
query_data = {} if select is None else {"select": select}
Expand Down
32 changes: 32 additions & 0 deletions tests/functional/api/test_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,38 @@ def test_upload_generic_package(tmp_path, project):
assert package.message == "201 Created"


def test_upload_generic_package_as_bytes(tmp_path, project):
path = tmp_path / file_name

path.write_text(file_content)

package = project.generic_packages.upload(
package_name=package_name,
package_version=package_version,
file_name=file_name,
data=path.read_bytes(),
)

assert isinstance(package, GenericPackage)
assert package.message == "201 Created"


def test_upload_generic_package_as_file(tmp_path, project):
path = tmp_path / file_name

path.write_text(file_content)

package = project.generic_packages.upload(
package_name=package_name,
package_version=package_version,
file_name=file_name,
data=path.open(mode="rb"),
)

assert isinstance(package, GenericPackage)
assert package.message == "201 Created"


def test_upload_generic_package_select(tmp_path, project):
path = tmp_path / file_name2
path.write_text(file_content)
Expand Down
69 changes: 69 additions & 0 deletions tests/unit/objects/test_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
import responses

from gitlab import exceptions as exc
from gitlab.v4.objects import (
GenericPackage,
GroupPackage,
Expand Down Expand Up @@ -281,6 +282,74 @@ def test_upload_generic_package(tmp_path, project, resp_upload_generic_package):
assert isinstance(package, GenericPackage)


def test_upload_generic_package_nonexistent_path(tmp_path, project):
with pytest.raises(exc.GitlabUploadError):
project.generic_packages.upload(
package_name=package_name,
package_version=package_version,
file_name=file_name,
path="bad",
)


def test_upload_generic_package_no_file_and_no_data(tmp_path, project):
path = tmp_path / file_name

path.write_text(file_content, encoding="utf-8")

with pytest.raises(exc.GitlabUploadError):
project.generic_packages.upload(
package_name=package_name,
package_version=package_version,
file_name=file_name,
)


def test_upload_generic_package_file_and_data(tmp_path, project):
path = tmp_path / file_name

path.write_text(file_content, encoding="utf-8")

with pytest.raises(exc.GitlabUploadError):
project.generic_packages.upload(
package_name=package_name,
package_version=package_version,
file_name=file_name,
path=path,
data=path.read_bytes(),
)


def test_upload_generic_package_bytes(tmp_path, project, resp_upload_generic_package):
path = tmp_path / file_name

path.write_text(file_content, encoding="utf-8")

package = project.generic_packages.upload(
package_name=package_name,
package_version=package_version,
file_name=file_name,
data=path.read_bytes(),
)

assert isinstance(package, GenericPackage)


def test_upload_generic_package_file(tmp_path, project, resp_upload_generic_package):
path = tmp_path / file_name

path.write_text(file_content, encoding="utf-8")

package = project.generic_packages.upload(
package_name=package_name,
package_version=package_version,
file_name=file_name,
data=path.open(mode="rb"),
)

assert isinstance(package, GenericPackage)


def test_download_generic_package(project, resp_download_generic_package):
package = project.generic_packages.download(
package_name=package_name,
Expand Down