Skip to content

Commit 62df3cc

Browse files
committed
feat(packages): Allow uploading bytes and files
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
1 parent 8d2d297 commit 62df3cc

File tree

5 files changed

+143
-13
lines changed

5 files changed

+143
-13
lines changed

gitlab/_backends/requests_backend.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import dataclasses
4-
from typing import Any, Dict, Optional, TYPE_CHECKING, Union
4+
from typing import Any, BinaryIO, Dict, Optional, TYPE_CHECKING, Union
55

66
import requests
77
from requests.structures import CaseInsensitiveDict
@@ -63,7 +63,7 @@ def client(self) -> requests.Session:
6363
@staticmethod
6464
def prepare_send_data(
6565
files: Optional[Dict[str, Any]] = None,
66-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
66+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
6767
raw: bool = False,
6868
) -> SendData:
6969
if files:
@@ -90,6 +90,9 @@ def prepare_send_data(
9090
if raw and post_data:
9191
return SendData(data=post_data, content_type="application/octet-stream")
9292

93+
if TYPE_CHECKING:
94+
assert not isinstance(post_data, BinaryIO)
95+
9396
return SendData(json=post_data, content_type="application/json")
9497

9598
def http_request(

gitlab/client.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@
33
import os
44
import re
55
import time
6-
from typing import Any, cast, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union
6+
from typing import (
7+
Any,
8+
BinaryIO,
9+
cast,
10+
Dict,
11+
List,
12+
Optional,
13+
Tuple,
14+
Type,
15+
TYPE_CHECKING,
16+
Union,
17+
)
718
from urllib import parse
819

920
import requests
@@ -644,7 +655,7 @@ def http_request(
644655
verb: str,
645656
path: str,
646657
query_data: Optional[Dict[str, Any]] = None,
647-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
658+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
648659
raw: bool = False,
649660
streamed: bool = False,
650661
files: Optional[Dict[str, Any]] = None,
@@ -1045,7 +1056,7 @@ def http_put(
10451056
self,
10461057
path: str,
10471058
query_data: Optional[Dict[str, Any]] = None,
1048-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
1059+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
10491060
raw: bool = False,
10501061
files: Optional[Dict[str, Any]] = None,
10511062
**kwargs: Any,

gitlab/v4/objects/packages.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@
55
"""
66

77
from pathlib import Path
8-
from typing import Any, Callable, cast, Iterator, Optional, TYPE_CHECKING, Union
8+
from typing import (
9+
Any,
10+
BinaryIO,
11+
Callable,
12+
cast,
13+
Iterator,
14+
Optional,
15+
TYPE_CHECKING,
16+
Union,
17+
)
918

1019
import requests
1120

@@ -46,8 +55,9 @@ def upload(
4655
package_name: str,
4756
package_version: str,
4857
file_name: str,
49-
path: Union[str, Path],
58+
path: Optional[Union[str, Path]] = None,
5059
select: Optional[str] = None,
60+
data: Optional[Union[bytes, BinaryIO]] = None,
5161
**kwargs: Any,
5262
) -> GenericPackage:
5363
"""Upload a file as a generic package.
@@ -64,19 +74,34 @@ def upload(
6474
Raises:
6575
GitlabConnectionError: If the server cannot be reached
6676
GitlabUploadError: If the file upload fails
67-
GitlabUploadError: If ``filepath`` cannot be read
77+
GitlabUploadError: If ``path`` cannot be read
78+
GitlabUploadError: If both ``path`` and ``data`` are passed
6879
6980
Returns:
7081
An object storing the metadata of the uploaded package.
7182
7283
https://docs.gitlab.com/ee/user/packages/generic_packages/
7384
"""
7485

75-
try:
76-
with open(path, "rb") as f:
77-
file_data = f.read()
78-
except OSError as e:
79-
raise exc.GitlabUploadError(f"Failed to read package file {path}") from e
86+
if path is None and data is None:
87+
raise exc.GitlabUploadError("No file contents or path specified")
88+
89+
if path is not None and data is not None:
90+
raise exc.GitlabUploadError("File contents and file path specified")
91+
92+
file_data: Optional[Union[bytes, BinaryIO]] = data
93+
94+
if not file_data:
95+
if TYPE_CHECKING:
96+
assert path is not None
97+
98+
try:
99+
with open(path, "rb") as f:
100+
file_data = f.read()
101+
except OSError as e:
102+
raise exc.GitlabUploadError(
103+
f"Failed to read package file {path}"
104+
) from e
80105

81106
url = f"{self._computed_path}/{package_name}/{package_version}/{file_name}"
82107
query_data = {} if select is None else {"select": select}

tests/functional/api/test_packages.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,38 @@ def test_upload_generic_package(tmp_path, project):
3838
assert package.message == "201 Created"
3939

4040

41+
def test_download_generic_package_bytes(tmp_path, project):
42+
path = tmp_path / file_name
43+
44+
path.write_text(file_content)
45+
46+
package = project.generic_packages.upload(
47+
package_name=package_name,
48+
package_version=package_version,
49+
file_name=file_name,
50+
data=path.read_bytes(),
51+
)
52+
53+
assert isinstance(package, GenericPackage)
54+
assert package.message == "201 Created"
55+
56+
57+
def test_download_generic_package_file(tmp_path, project):
58+
path = tmp_path / file_name
59+
60+
path.write_text(file_content)
61+
62+
package = project.generic_packages.upload(
63+
package_name=package_name,
64+
package_version=package_version,
65+
file_name=file_name,
66+
data=path.open(mode="rb"),
67+
)
68+
69+
assert isinstance(package, GenericPackage)
70+
assert package.message == "201 Created"
71+
72+
4173
def test_upload_generic_package_select(tmp_path, project):
4274
path = tmp_path / file_name2
4375
path.write_text(file_content)

tests/unit/objects/test_packages.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
import responses
88

9+
from gitlab import exceptions as exc
910
from gitlab.v4.objects import (
1011
GenericPackage,
1112
GroupPackage,
@@ -287,6 +288,64 @@ def test_upload_generic_package(tmp_path, project, resp_upload_generic_package):
287288
assert isinstance(package, GenericPackage)
288289

289290

291+
def test_upload_generic_package_no_file_and_no_data(tmp_path, project):
292+
path = tmp_path / file_name
293+
294+
path.write_text(file_content, encoding="utf-8")
295+
296+
with pytest.raises(exc.GitlabUploadError):
297+
project.generic_packages.upload(
298+
package_name=package_name,
299+
package_version=package_version,
300+
file_name=file_name,
301+
)
302+
303+
304+
def test_upload_generic_package_file_and_data(tmp_path, project):
305+
path = tmp_path / file_name
306+
307+
path.write_text(file_content, encoding="utf-8")
308+
309+
with pytest.raises(exc.GitlabUploadError):
310+
project.generic_packages.upload(
311+
package_name=package_name,
312+
package_version=package_version,
313+
file_name=file_name,
314+
path=path,
315+
data=path.read_bytes(),
316+
)
317+
318+
319+
def test_upload_generic_package_bytes(tmp_path, project, resp_upload_generic_package):
320+
path = tmp_path / file_name
321+
322+
path.write_text(file_content, encoding="utf-8")
323+
324+
package = project.generic_packages.upload(
325+
package_name=package_name,
326+
package_version=package_version,
327+
file_name=file_name,
328+
data=path.read_bytes(),
329+
)
330+
331+
assert isinstance(package, GenericPackage)
332+
333+
334+
def test_upload_generic_package_file(tmp_path, project, resp_upload_generic_package):
335+
path = tmp_path / file_name
336+
337+
path.write_text(file_content, encoding="utf-8")
338+
339+
package = project.generic_packages.upload(
340+
package_name=package_name,
341+
package_version=package_version,
342+
file_name=file_name,
343+
data=path.open(mode="rb"),
344+
)
345+
346+
assert isinstance(package, GenericPackage)
347+
348+
290349
def test_download_generic_package(project, resp_download_generic_package):
291350
package = project.generic_packages.download(
292351
package_name=package_name,

0 commit comments

Comments
 (0)