Skip to content

Commit a2beef3

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 a2beef3

File tree

5 files changed

+112
-12
lines changed

5 files changed

+112
-12
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: 31 additions & 7 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.
@@ -72,11 +82,25 @@ def upload(
7282
https://docs.gitlab.com/ee/user/packages/generic_packages/
7383
"""
7484

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

81105
url = f"{self._computed_path}/{package_name}/{package_version}/{file_name}"
82106
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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,36 @@ def test_upload_generic_package(tmp_path, project, resp_upload_generic_package):
287287
assert isinstance(package, GenericPackage)
288288

289289

290+
def test_upload_generic_package_bytes(tmp_path, project, resp_upload_generic_package):
291+
path = tmp_path / file_name
292+
293+
path.write_text(file_content, encoding="utf-8")
294+
295+
package = project.generic_packages.upload(
296+
package_name=package_name,
297+
package_version=package_version,
298+
file_name=file_name,
299+
data=path.read_bytes(),
300+
)
301+
302+
assert isinstance(package, GenericPackage)
303+
304+
305+
def test_upload_generic_package_file(tmp_path, project, resp_upload_generic_package):
306+
path = tmp_path / file_name
307+
308+
path.write_text(file_content, encoding="utf-8")
309+
310+
package = project.generic_packages.upload(
311+
package_name=package_name,
312+
package_version=package_version,
313+
file_name=file_name,
314+
data=path.open(mode="rb"),
315+
)
316+
317+
assert isinstance(package, GenericPackage)
318+
319+
290320
def test_download_generic_package(project, resp_download_generic_package):
291321
package = project.generic_packages.download(
292322
package_name=package_name,

0 commit comments

Comments
 (0)