Skip to content

Commit e8fa2f8

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 9b6d89e commit e8fa2f8

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.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
@@ -618,7 +629,7 @@ def http_request(
618629
verb: str,
619630
path: str,
620631
query_data: Optional[Dict[str, Any]] = None,
621-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
632+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
622633
raw: bool = False,
623634
streamed: bool = False,
624635
files: Optional[Dict[str, Any]] = None,
@@ -999,7 +1010,7 @@ def http_put(
9991010
self,
10001011
path: str,
10011012
query_data: Optional[Dict[str, Any]] = None,
1002-
post_data: Optional[Union[Dict[str, Any], bytes]] = None,
1013+
post_data: Optional[Union[Dict[str, Any], bytes, BinaryIO]] = None,
10031014
raw: bool = False,
10041015
files: Optional[Dict[str, Any]] = None,
10051016
**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)