Skip to content

Commit 34c43de

Browse files
ernestasknejch
authored andcommitted
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 a788cff commit 34c43de

File tree

5 files changed

+153
-13
lines changed

5 files changed

+153
-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 import PreparedRequest
@@ -94,7 +94,7 @@ def client(self) -> requests.Session:
9494
@staticmethod
9595
def prepare_send_data(
9696
files: Optional[Dict[str, Any]] = None,
97-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
97+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
9898
raw: bool = False,
9999
) -> SendData:
100100
if files:
@@ -121,6 +121,9 @@ def prepare_send_data(
121121
if raw and post_data:
122122
return SendData(data=post_data, content_type="application/octet-stream")
123123

124+
if TYPE_CHECKING:
125+
assert not isinstance(post_data, BinaryIO)
126+
124127
return SendData(json=post_data, content_type="application/json")
125128

126129
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
@@ -612,7 +623,7 @@ def http_request(
612623
verb: str,
613624
path: str,
614625
query_data: Optional[Dict[str, Any]] = None,
615-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
626+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
616627
raw: bool = False,
617628
streamed: bool = False,
618629
files: Optional[Dict[str, Any]] = None,
@@ -993,7 +1004,7 @@ def http_put(
9931004
self,
9941005
path: str,
9951006
query_data: Optional[Dict[str, Any]] = None,
996-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
1007+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
9971008
raw: bool = False,
9981009
files: Optional[Dict[str, Any]] = None,
9991010
**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: 69 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,
@@ -281,6 +282,74 @@ def test_upload_generic_package(tmp_path, project, resp_upload_generic_package):
281282
assert isinstance(package, GenericPackage)
282283

283284

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

0 commit comments

Comments
 (0)