From 61e0faec2014919e0a2e79106089f6838be8ad0e Mon Sep 17 00:00:00 2001 From: Ernestas Kulik Date: Thu, 3 Aug 2023 11:38:45 +0300 Subject: [PATCH] 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 https://github.com/python-gitlab/python-gitlab/issues/1815 --- gitlab/_backends/requests_backend.py | 7 ++- gitlab/client.py | 17 +++++-- gitlab/v4/objects/packages.py | 41 ++++++++++++---- tests/functional/api/test_packages.py | 32 +++++++++++++ tests/unit/objects/test_packages.py | 69 +++++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 13 deletions(-) diff --git a/gitlab/_backends/requests_backend.py b/gitlab/_backends/requests_backend.py index 8b7d740a4..79e3cbf12 100644 --- a/gitlab/_backends/requests_backend.py +++ b/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 @@ -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: @@ -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( diff --git a/gitlab/client.py b/gitlab/client.py index 80a062489..7493e380c 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -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 @@ -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, @@ -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, diff --git a/gitlab/v4/objects/packages.py b/gitlab/v4/objects/packages.py index fc5b91075..4ede5d9b5 100644 --- a/gitlab/v4/objects/packages.py +++ b/gitlab/v4/objects/packages.py @@ -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 @@ -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. @@ -64,7 +74,8 @@ 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. @@ -72,11 +83,25 @@ def upload( 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} diff --git a/tests/functional/api/test_packages.py b/tests/functional/api/test_packages.py index e83d6b94c..373b9655f 100644 --- a/tests/functional/api/test_packages.py +++ b/tests/functional/api/test_packages.py @@ -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) diff --git a/tests/unit/objects/test_packages.py b/tests/unit/objects/test_packages.py index 2fe835116..869b50fc5 100644 --- a/tests/unit/objects/test_packages.py +++ b/tests/unit/objects/test_packages.py @@ -6,6 +6,7 @@ import pytest import responses +from gitlab import exceptions as exc from gitlab.v4.objects import ( GenericPackage, GroupPackage, @@ -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,