Skip to content

Commit

Permalink
feat(packages): Allow uploading bytes and files
Browse files Browse the repository at this point in the history
This commit adds a keyword argument to GenericPackageManager.upload() to
allow uploading bytes and file-like objects to the generic package
registry. That necessitates changing file path to be a keyword argument
as well, which then cascades into a whole slew of checks to not allow
passing both and to not allow uploading file-like objects as JSON data.

Closes #1815
  • Loading branch information
ernestask authored and nejch committed Oct 12, 2023
1 parent a788cff commit 61e0fae
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 13 deletions.
7 changes: 5 additions & 2 deletions gitlab/_backends/requests_backend.py
@@ -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
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
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
Expand Up @@ -38,6 +38,38 @@ def test_upload_generic_package(tmp_path, project):
assert package.message == "201 Created"


def test_download_generic_package_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_download_generic_package_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
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

0 comments on commit 61e0fae

Please sign in to comment.