Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-azure-core for openSUSE:Factory checked in at 2024-02-11 15:46:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-azure-core (Old) and /work/SRC/openSUSE:Factory/.python-azure-core.new.1815 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-azure-core" Sun Feb 11 15:46:00 2024 rev:47 rq:1145587 version:1.30.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-azure-core/python-azure-core.changes 2024-01-24 19:06:27.649939306 +0100 +++ /work/SRC/openSUSE:Factory/.python-azure-core.new.1815/python-azure-core.changes 2024-02-11 15:46:01.443645595 +0100 @@ -1,0 +2,8 @@ +Wed Feb 7 09:36:55 UTC 2024 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- New upstream release + + Version 1.30.0 + + For detailed information about changes see the + CHANGELOG.md file provided with this package + +------------------------------------------------------------------- Old: ---- azure-core-1.29.7.tar.gz New: ---- azure-core-1.30.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-azure-core.spec ++++++ --- /var/tmp/diff_new_pack.zquS64/_old 2024-02-11 15:46:01.963664292 +0100 +++ /var/tmp/diff_new_pack.zquS64/_new 2024-02-11 15:46:01.963664292 +0100 @@ -21,7 +21,7 @@ %define skip_python2 1 %endif Name: python-azure-core -Version: 1.29.7 +Version: 1.30.0 Release: 0 Summary: Microsoft Azure Core Library for Python License: MIT ++++++ azure-core-1.29.7.tar.gz -> azure-core-1.30.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/CHANGELOG.md new/azure-core-1.30.0/CHANGELOG.md --- old/azure-core-1.29.7/CHANGELOG.md 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/CHANGELOG.md 2024-01-31 22:55:17.000000000 +0100 @@ -1,5 +1,12 @@ # Release History +## 1.30.0 (2024-02-01) + +### Features Added + +- Support tuple input for file values to `azure.core.rest.HttpRequest` #33948 +- Support tuple input to `files` with duplicate field names `azure.core.rest.HttpRequest` #34021 + ## 1.29.7 (2024-01-18) ### Other Changes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/PKG-INFO new/azure-core-1.30.0/PKG-INFO --- old/azure-core-1.29.7/PKG-INFO 2024-01-18 16:40:05.470854300 +0100 +++ new/azure-core-1.30.0/PKG-INFO 2024-01-31 22:56:05.936185100 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: azure-core -Version: 1.29.7 +Version: 1.30.0 Summary: Microsoft Azure Core Library for Python Home-page: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core Author: Microsoft Corporation @@ -286,6 +286,13 @@ # Release History +## 1.30.0 (2024-02-01) + +### Features Added + +- Support tuple input for file values to `azure.core.rest.HttpRequest` #33948 +- Support tuple input to `files` with duplicate field names `azure.core.rest.HttpRequest` #34021 + ## 1.29.7 (2024-01-18) ### Other Changes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/azure/core/_version.py new/azure-core-1.30.0/azure/core/_version.py --- old/azure-core-1.29.7/azure/core/_version.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/azure/core/_version.py 2024-01-31 22:55:17.000000000 +0100 @@ -9,4 +9,4 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "1.29.7" +VERSION = "1.30.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/azure/core/pipeline/transport/_aiohttp.py new/azure-core-1.30.0/azure/core/pipeline/transport/_aiohttp.py --- old/azure-core-1.29.7/azure/core/pipeline/transport/_aiohttp.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/azure/core/pipeline/transport/_aiohttp.py 2024-01-31 22:55:17.000000000 +0100 @@ -56,7 +56,7 @@ from ._base import HttpRequest from ._base_async import AsyncHttpTransport, AsyncHttpResponse, _ResponseStopIteration -from ...utils._pipeline_transport_rest_shared import _aiohttp_body_helper +from ...utils._pipeline_transport_rest_shared import _aiohttp_body_helper, get_file_items from .._tools import is_rest as _is_rest from .._tools_async import ( handle_no_stream_rest_response as _handle_no_stream_rest_response, @@ -180,7 +180,7 @@ """ if request.files: form_data = aiohttp.FormData(request.data or {}) - for form_file, data in request.files.items(): + for form_file, data in get_file_items(request.files): content_type = data[2] if len(data) > 2 else None try: form_data.add_field(form_file, data[1], filename=data[0], content_type=content_type) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/azure/core/pipeline/transport/_base.py new/azure-core-1.30.0/azure/core/pipeline/transport/_base.py --- old/azure-core-1.29.7/azure/core/pipeline/transport/_base.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/azure/core/pipeline/transport/_base.py 2024-01-31 22:55:17.000000000 +0100 @@ -76,6 +76,7 @@ if TYPE_CHECKING: # We need a transport to define a pipeline, this "if" avoid a circular import from azure.core.pipeline import Pipeline + from azure.core.rest._helpers import FileContent _LOGGER = logging.getLogger(__name__) @@ -249,7 +250,7 @@ self.data = value @staticmethod - def _format_data(data: Union[str, IO]) -> Union[Tuple[None, str], Tuple[Optional[str], IO, str]]: + def _format_data(data: Union[str, IO]) -> Union[Tuple[Optional[str], str], Tuple[Optional[str], FileContent, str]]: """Format field data according to whether it is a stream or a string for a form-data request. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/azure/core/rest/_helpers.py new/azure-core-1.30.0/azure/core/rest/_helpers.py --- old/azure-core-1.29.7/azure/core/rest/_helpers.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/azure/core/rest/_helpers.py 2024-01-31 22:55:17.000000000 +0100 @@ -52,6 +52,7 @@ _prepare_multipart_body_helper, _serialize_request, _format_data_helper, + get_file_items, ) if TYPE_CHECKING: @@ -66,7 +67,15 @@ ParamsType = Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]] FileContent = Union[str, bytes, IO[str], IO[bytes]] -FileType = Tuple[Optional[str], FileContent] + +FileType = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], +] FilesType = Union[Mapping[str, FileType], Sequence[Tuple[str, FileType]]] @@ -104,9 +113,9 @@ return default_headers, body -def set_multipart_body(files): - formatted_files = {f: _format_data_helper(d) for f, d in files.items() if d is not None} - return {}, formatted_files +def set_multipart_body(files: FilesType): + formatted_files = [(f, _format_data_helper(d)) for f, d in get_file_items(files) if d is not None] + return {}, dict(formatted_files) if isinstance(files, Mapping) else formatted_files def set_xml_body(content): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/azure/core/utils/_pipeline_transport_rest_shared.py new/azure-core-1.30.0/azure/core/utils/_pipeline_transport_rest_shared.py --- old/azure-core-1.29.7/azure/core/utils/_pipeline_transport_rest_shared.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/azure/core/utils/_pipeline_transport_rest_shared.py 2024-01-31 22:55:17.000000000 +0100 @@ -5,6 +5,7 @@ # license information. # -------------------------------------------------------------------------- from __future__ import absolute_import +from collections.abc import Mapping from io import BytesIO from email.message import Message @@ -50,6 +51,7 @@ from azure.core.pipeline.transport._base import ( _HttpResponseBase as PipelineTransportHttpResponseBase, ) + from azure.core.rest._helpers import FilesType, FileType, FileContent binary_type = str @@ -333,7 +335,7 @@ return responses -def _format_data_helper(data: Union[str, IO]) -> Union[Tuple[None, str], Tuple[Optional[str], IO, str]]: +def _format_data_helper(data: "FileType") -> Union[Tuple[Optional[str], str], Tuple[Optional[str], "FileContent", str]]: """Helper for _format_data. Format field data according to whether it is a stream or @@ -344,16 +346,34 @@ :rtype: tuple[str, IO, str] or tuple[None, str] :return: A tuple of (data name, data IO, "application/octet-stream") or (None, data str) """ - if hasattr(data, "read"): - data = cast(IO, data) - data_name = None - try: - if data.name[0] != "<" and data.name[-1] != ">": - data_name = os.path.basename(data.name) - except (AttributeError, TypeError): - pass - return (data_name, data, "application/octet-stream") - return (None, cast(str, data)) + content_type: Optional[str] = None + filename: Optional[str] = None + if isinstance(data, tuple): + if len(data) == 2: + # Filename and file bytes are included + filename, file_bytes = cast(Tuple[Optional[str], "FileContent"], data) + elif len(data) == 3: + # Filename, file object, and content_type are included + filename, file_bytes, content_type = cast(Tuple[Optional[str], "FileContent", str], data) + else: + raise ValueError( + "Unexpected data format. Expected file, or tuple of (filename, file_bytes) or " + "(filename, file_bytes, content_type)." + ) + else: + # here we just get the file content + if hasattr(data, "read"): + data = cast(IO, data) + try: + if data.name[0] != "<" and data.name[-1] != ">": + filename = os.path.basename(data.name) + except (AttributeError, TypeError): + pass + content_type = "application/octet-stream" + file_bytes = data + if content_type: + return (filename, file_bytes, content_type) + return (filename, cast(str, file_bytes)) def _aiohttp_body_helper( @@ -390,3 +410,11 @@ response._decompressed_content = True return response._content return response._content + + +def get_file_items(files: "FilesType") -> Sequence[Tuple[str, "FileType"]]: + if isinstance(files, Mapping): + # casting because ItemsView technically isn't a Sequence, even + # though realistically it is ordered python 3.7 and after + return cast(Sequence[Tuple[str, "FileType"]], files.items()) + return files diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/azure_core.egg-info/PKG-INFO new/azure-core-1.30.0/azure_core.egg-info/PKG-INFO --- old/azure-core-1.29.7/azure_core.egg-info/PKG-INFO 2024-01-18 16:40:05.000000000 +0100 +++ new/azure-core-1.30.0/azure_core.egg-info/PKG-INFO 2024-01-31 22:56:05.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: azure-core -Version: 1.29.7 +Version: 1.30.0 Summary: Microsoft Azure Core Library for Python Home-page: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core Author: Microsoft Corporation @@ -286,6 +286,13 @@ # Release History +## 1.30.0 (2024-02-01) + +### Features Added + +- Support tuple input for file values to `azure.core.rest.HttpRequest` #33948 +- Support tuple input to `files` with duplicate field names `azure.core.rest.HttpRequest` #34021 + ## 1.29.7 (2024-01-18) ### Other Changes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/tests/async_tests/test_rest_http_request_async.py new/azure-core-1.30.0/tests/async_tests/test_rest_http_request_async.py --- old/azure-core-1.29.7/tests/async_tests/test_rest_http_request_async.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/tests/async_tests/test_rest_http_request_async.py 2024-01-31 22:55:17.000000000 +0100 @@ -9,6 +9,7 @@ import pytest from azure.core.rest import HttpRequest import collections.abc +from utils import NamedIo @pytest.fixture @@ -94,3 +95,40 @@ await assert_aiterator_body(request, b"test 123") # in this case, request._data is what we end up passing to the requests transport assert isinstance(request._data, collections.abc.AsyncIterable) + + +@pytest.mark.asyncio +async def test_multipart_tuple_input_multiple_same_name(client): + request = HttpRequest( + "POST", + url="/multipart/tuple-input-multiple-same-name", + files=[ + ("file", ("firstFileName", NamedIo("firstFile"), "image/pdf")), + ("file", ("secondFileName", NamedIo("secondFile"), "image/png")), + ], + ) + (await client.send_request(request)).raise_for_status() + + +@pytest.mark.asyncio +async def test_multipart_tuple_input_multiple_same_name_with_tuple_file_value(client): + request = HttpRequest( + "POST", + url="/multipart/tuple-input-multiple-same-name-with-tuple-file-value", + files=[("images", ("foo.png", NamedIo("notMyName.pdf"), "image/png")), ("images", NamedIo("foo.png"))], + ) + (await client.send_request(request)).raise_for_status() + + +@pytest.mark.asyncio +async def test_data_and_file_input_same_name(client): + request = HttpRequest( + "POST", + url="/multipart/data-and-file-input-same-name", + data={"message": "Hello, world!"}, + files=[ + ("file", ("firstFileName", NamedIo("firstFile"), "image/pdf")), + ("file", ("secondFileName", NamedIo("secondFile"), "image/png")), + ], + ) + (await client.send_request(request)).raise_for_status() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/tests/test_rest_http_request.py new/azure-core-1.30.0/tests/test_rest_http_request.py --- old/azure-core-1.29.7/tests/test_rest_http_request.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/tests/test_rest_http_request.py 2024-01-31 22:55:17.000000000 +0100 @@ -23,6 +23,7 @@ from azure.core.pipeline._tools import is_rest from rest_client import MockRestClient from azure.core import PipelineClient +from utils import NamedIo @pytest.fixture @@ -515,29 +516,95 @@ HttpRequest(method="PUT", url="http://www.example.com", content=data_stream) # ensure we can make this HttpRequest -# NOTE: For files, we don't allow list of tuples yet, just dict. Will uncomment when we add this capability -# def test_multipart_multiple_files_single_input_content(): -# files = [ -# ("file", io.BytesIO(b"<file content 1>")), -# ("file", io.BytesIO(b"<file content 2>")), -# ] -# request = HttpRequest("POST", url="http://example.org", files=files) -# assert request.headers == { -# "Content-Length": "271", -# "Content-Type": "multipart/form-data; boundary=+++", -# } -# assert request.content == b"".join( -# [ -# b"--+++\r\n", -# b'Content-Disposition: form-data; name="file"; filename="upload"\r\n', -# b"Content-Type: application/octet-stream\r\n", -# b"\r\n", -# b"<file content 1>\r\n", -# b"--+++\r\n", -# b'Content-Disposition: form-data; name="file"; filename="upload"\r\n', -# b"Content-Type: application/octet-stream\r\n", -# b"\r\n", -# b"<file content 2>\r\n", -# b"--+++--\r\n", -# ] -# ) +@pytest.fixture +def filebytes(): + file_path = os.path.join(os.path.dirname(__file__), "./conftest.py") + return open(file_path, "rb") + + +def test_multipart_bytes(filebytes): + request = HttpRequest("POST", url="http://example.org", files={"file": filebytes}) + + assert request.content == {"file": ("conftest.py", filebytes, "application/octet-stream")} + + +def test_multipart_filename_and_bytes(filebytes): + files = ("specifiedFileName", filebytes) + request = HttpRequest("POST", url="http://example.org", files={"file": files}) + + assert request.content == {"file": ("specifiedFileName", filebytes)} + + +def test_multipart_filename_and_bytes_and_content_type(filebytes): + files = ("specifiedFileName", filebytes, "application/json") + request = HttpRequest("POST", url="http://example.org", files={"file": files}) + + assert request.content == {"file": ("specifiedFileName", filebytes, "application/json")} + + +def test_multipart_incorrect_tuple_entry(filebytes): + files = ("specifiedFileName", filebytes, "application/json", "extra") + with pytest.raises(ValueError): + HttpRequest("POST", url="http://example.org", files={"file": files}) + + +def test_multipart_tuple_input_single(filebytes): + request = HttpRequest("POST", url="http://example.org", files=[("file", filebytes)]) + + assert request.content == [("file", ("conftest.py", filebytes, "application/octet-stream"))] + + +def test_multipart_tuple_input_multiple(filebytes): + request = HttpRequest("POST", url="http://example.org", files=[("file", filebytes), ("file2", filebytes)]) + + assert request.content == [ + ("file", ("conftest.py", filebytes, "application/octet-stream")), + ("file2", ("conftest.py", filebytes, "application/octet-stream")), + ] + + +def test_multipart_tuple_input_multiple_with_filename_and_content_type(filebytes): + request = HttpRequest( + "POST", + url="http://example.org", + files=[("file", ("first file", filebytes, "image/pdf")), ("file2", ("second file", filebytes, "image/png"))], + ) + + assert request.content == [ + ("file", ("first file", filebytes, "image/pdf")), + ("file2", ("second file", filebytes, "image/png")), + ] + + +def test_multipart_tuple_input_multiple_same_name(client): + request = HttpRequest( + "POST", + url="/multipart/tuple-input-multiple-same-name", + files=[ + ("file", ("firstFileName", NamedIo("firstFile"), "image/pdf")), + ("file", ("secondFileName", NamedIo("secondFile"), "image/png")), + ], + ) + client.send_request(request).raise_for_status() + + +def test_multipart_tuple_input_multiple_same_name_with_tuple_file_value(client): + request = HttpRequest( + "POST", + url="/multipart/tuple-input-multiple-same-name-with-tuple-file-value", + files=[("images", ("foo.png", NamedIo("notMyName.pdf"), "image/png")), ("images", NamedIo("foo.png"))], + ) + client.send_request(request).raise_for_status() + + +def test_data_and_file_input_same_name(client): + request = HttpRequest( + "POST", + url="/multipart/data-and-file-input-same-name", + data={"message": "Hello, world!"}, + files=[ + ("file", ("firstFileName", NamedIo("firstFile"), "image/pdf")), + ("file", ("secondFileName", NamedIo("secondFile"), "image/png")), + ], + ) + client.send_request(request).raise_for_status() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/tests/testserver_tests/coretestserver/coretestserver/test_routes/multipart.py new/azure-core-1.30.0/tests/testserver_tests/coretestserver/coretestserver/test_routes/multipart.py --- old/azure-core-1.29.7/tests/testserver_tests/coretestserver/coretestserver/test_routes/multipart.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/tests/testserver_tests/coretestserver/coretestserver/test_routes/multipart.py 2024-01-31 22:55:17.000000000 +0100 @@ -4,7 +4,6 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for # license information. # ------------------------------------------------------------------------- -from copy import copy from flask import ( Response, Blueprint, @@ -147,3 +146,50 @@ body_as_str.encode("ascii"), content_type="multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed", ) + + +@multipart_api.route("/tuple-input-multiple-same-name", methods=["POST"]) +def tuple_input_multiple_same_name(): + assert_with_message("content type", multipart_header_start, request.content_type[: len(multipart_header_start)]) + + files = request.files.getlist("file") + assert_with_message("num files", 2, len(files)) + + file1 = files[0] + assert_with_message("file content type", "image/pdf", file1.content_type) + assert_with_message("filename", "firstFileName", file1.filename) + + file2 = files[1] + assert_with_message("file content type", "image/png", file2.content_type) + assert_with_message("filename", "secondFileName", file2.filename) + return Response(status=200) + + +@multipart_api.route("/tuple-input-multiple-same-name-with-tuple-file-value", methods=["POST"]) +def test_input_multiple_same_name_with_tuple_file_value(): + assert_with_message("content type", multipart_header_start, request.content_type[: len(multipart_header_start)]) + + images = request.files.getlist("images") + assert_with_message("num images", 2, len(images)) + + tuple_image = images[0] + assert_with_message("image content type", "image/png", tuple_image.content_type) + assert_with_message("filename", "foo.png", tuple_image.filename) + + single_image = images[1] + assert_with_message("file content type", "application/octet-stream", single_image.content_type) + assert_with_message("filename", "foo.png", single_image.filename) + return Response(status=200) + + +@multipart_api.route("/data-and-file-input-same-name", methods=["POST"]) +def data_and_file_input_same_name(): + assert_with_message("content type", multipart_header_start, request.content_type[: len(multipart_header_start)]) + + # use call to this function to check files + tuple_input_multiple_same_name() + + assert_with_message("data items num", 1, len(request.form.keys())) + assert_with_message("message", "Hello, world!", request.form["message"]) + + return Response(status=200) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-core-1.29.7/tests/utils.py new/azure-core-1.30.0/tests/utils.py --- old/azure-core-1.29.7/tests/utils.py 2024-01-18 16:39:24.000000000 +0100 +++ new/azure-core-1.30.0/tests/utils.py 2024-01-31 22:55:17.000000000 +0100 @@ -5,6 +5,7 @@ # ------------------------------------------------------------------------- import pytest import types +import io ############################## LISTS USED TO PARAMETERIZE TESTS ############################## from azure.core.rest import HttpRequest as RestHttpRequest @@ -172,3 +173,9 @@ if not attr in vars(old_response): with pytest.raises(AttributeError): setattr(response, attr, "new_value") + + +class NamedIo(io.BytesIO): + def __init__(self, name: str, *args, **kwargs): + super(NamedIo, self).__init__(*args, **kwargs) + self.name = name