From 99c56676f3aa3a340165f803f8e7a7c2a3474a03 Mon Sep 17 00:00:00 2001 From: antisch Date: Tue, 24 Mar 2020 18:01:30 -0700 Subject: [PATCH 1/8] Support multipart pipeline context --- .../azure-core/azure/core/pipeline/_base.py | 9 +-- .../azure/core/pipeline/_base_async.py | 9 +-- .../test_basic_transport.py | 61 +++++++++++++++++++ .../azure-core/tests/test_basic_transport.py | 48 +++++++++++++++ 4 files changed, 119 insertions(+), 8 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/_base.py b/sdk/core/azure-core/azure/core/pipeline/_base.py index 854dbf24c199..f722a4f69da0 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/_base.py @@ -157,8 +157,8 @@ def __exit__(self, *exc_details): # pylint: disable=arguments-differ self._transport.__exit__(*exc_details) @staticmethod - def _prepare_multipart_mixed_request(request): - # type: (HTTPRequestType) -> None + def _prepare_multipart_mixed_request(request, **kwargs): + # type: (HTTPRequestType, Any) -> None """Will execute the multipart policies. Does nothing if "set_multipart_mixed" was never called. @@ -174,7 +174,7 @@ def _prepare_multipart_mixed_request(request): import concurrent.futures def prepare_requests(req): - context = PipelineContext(None) + context = PipelineContext(None, **kwargs) pipeline_request = PipelineRequest(req, context) for policy in policies: _await_result(policy.on_request, pipeline_request) @@ -194,7 +194,8 @@ def run(self, request, **kwargs): :return: The PipelineResponse object :rtype: ~azure.core.pipeline.PipelineResponse """ - self._prepare_multipart_mixed_request(request) + multipart_options = kwargs.pop("multipart_options", None) or {} + self._prepare_multipart_mixed_request(request, **multipart_options) request.prepare_multipart_body() # type: ignore context = PipelineContext(self._transport, **kwargs) pipeline_request = PipelineRequest( diff --git a/sdk/core/azure-core/azure/core/pipeline/_base_async.py b/sdk/core/azure-core/azure/core/pipeline/_base_async.py index d58ed8292bc6..8e4c7ccbb690 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_base_async.py +++ b/sdk/core/azure-core/azure/core/pipeline/_base_async.py @@ -168,8 +168,8 @@ async def __aenter__(self) -> "AsyncPipeline": async def __aexit__(self, *exc_details): # pylint: disable=arguments-differ await self._transport.__aexit__(*exc_details) - async def _prepare_multipart_mixed_request(self, request): - # type: (HTTPRequestType) -> None + async def _prepare_multipart_mixed_request(self, request, **kwargs): + # type: (HTTPRequestType, Any) -> None """Will execute the multipart policies. Does nothing if "set_multipart_mixed" was never called. @@ -182,7 +182,7 @@ async def _prepare_multipart_mixed_request(self, request): policies = multipart_mixed_info[1] # type: List[SansIOHTTPPolicy] async def prepare_requests(req): - context = PipelineContext(None) + context = PipelineContext(None, **kwargs) pipeline_request = PipelineRequest(req, context) for policy in policies: await _await_result(policy.on_request, pipeline_request) @@ -201,7 +201,8 @@ async def run(self, request: HTTPRequestType, **kwargs: Any): :return: The PipelineResponse object. :rtype: ~azure.core.pipeline.PipelineResponse """ - await self._prepare_multipart_mixed_request(request) + multipart_options = kwargs.pop("multipart_options", None) or {} + await self._prepare_multipart_mixed_request(request, **multipart_options) request.prepare_multipart_body() # type: ignore context = PipelineContext(self._transport, **kwargs) pipeline_request = PipelineRequest(request, context) diff --git a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py index ba2568d2108a..7c58f39a39af 100644 --- a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py @@ -87,6 +87,67 @@ async def on_request(self, request): ) +@pytest.mark.asyncio +async def test_multipart_send_with_context(): + + # transport = mock.MagicMock(spec=AsyncHttpTransport) + # MagicMock support async cxt manager only after 3.8 + # https://github.com/python/cpython/pull/9296 + + class MockAsyncHttpTransport(AsyncHttpTransport): + async def __aenter__(self): return self + async def __aexit__(self, *args): pass + async def open(self): pass + async def close(self): pass + async def send(self, request, **kwargs): pass + + transport = MockAsyncHttpTransport() + header_policy = HeadersPolicy() + + class RequestPolicy(object): + async def on_request(self, request): + # type: (PipelineRequest) -> None + request.http_request.headers['x-ms-date'] = 'Thu, 14 Jun 2018 16:46:54 GMT' + + req0 = HttpRequest("DELETE", "/container0/blob0") + req1 = HttpRequest("DELETE", "/container1/blob1") + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + req0, + req1, + policies=[header_policy, RequestPolicy()], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" # Fix it so test are deterministic + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request, multipart_options={'headers': {'Accept': 'application/json'}}) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'Accept: application/json\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'Accept: application/json\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + @pytest.mark.asyncio async def test_multipart_receive(): diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 00873c57c7e7..e9a8a879693a 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -210,6 +210,54 @@ def test_multipart_send(): ) +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_context(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + req0 = HttpRequest("DELETE", "/container0/blob0") + req1 = HttpRequest("DELETE", "/container1/blob1") + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + req0, + req1, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" # Fix it so test are deterministic + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request, multipart_options={'headers': {'Accept': 'application/json'}}) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'Accept: application/json\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'Accept: application/json\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + def test_multipart_receive(): class MockResponse(HttpResponse): From 2e87d32dfadfb5edb226167c8856ac1b8388516e Mon Sep 17 00:00:00 2001 From: antisch Date: Wed, 25 Mar 2020 08:37:57 -0700 Subject: [PATCH 2/8] Support sending multipart changesets --- .../azure/core/pipeline/transport/_base.py | 108 +++-- .../test_basic_transport.py | 371 +++++++++++++++--- .../azure-core/tests/test_basic_transport.py | 352 +++++++++++++++++ 3 files changed, 750 insertions(+), 81 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py index 711d9902bef3..6dd179e0f72f 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py @@ -37,6 +37,7 @@ import os import time import copy +import uuid try: binary_type = str @@ -271,6 +272,36 @@ def _format_data(data): pass return (data_name, data, "application/octet-stream") return (None, cast(str, data)) + + def _calculate_changesets(self): + # type: () -> Tuple[Dict[int, Optional[str]], List[int]] + """Convert the changeset ranges into start/end indexes.""" + changesets = self.multipart_mixed_info[3] # type: List[Union[Tuple[int, int, str], Tuple[int, int]]] + start = {} + end = [] + validate_indexes = [] + for changeset in changesets: + try: + start_index, end_index, boundary = changeset + except ValueError: + start_index, end_index = changeset + boundary = None + validate_indexes.extend(list(range(start_index, end_index + 1))) + start[start_index] = boundary + end.append(end_index) + if len(validate_indexes) != len(set(validate_indexes)): + raise ValueError("Changesets must not overlap.") + return start, end + + def _add_message_part(self, index, message, request): + # type: (int, Message, HttpRequest) -> None + """Add each request to the message batch.""" + part_message = Message() + part_message.add_header("Content-Type", "application/http") + part_message.add_header("Content-Transfer-Encoding", "binary") + part_message.add_header("Content-ID", str(index)) + part_message.set_payload(request.serialize()) + message.attach(part_message) def format_parameters(self, params): # type: (Dict[str, str]) -> None @@ -377,13 +408,19 @@ def set_multipart_mixed(self, *requests, **kwargs): :keyword list[SansIOHTTPPolicy] policies: SansIOPolicy to apply at preparation time :keyword str boundary: Optional boundary - + :keyword changesets: Optional list of tuples marking the change sets within the request list. + Each tuple should contain two integers: the start and inclusive-end indexes of each change set + within the request list. Optionally, the tuple can also contain a changeset boundary string. + Any request that fall outside the changeset ranges will be added to the batch normally. + Change set ranges must not overlap. + :paramtype changesets: List[Union[Tuple[int, int, str], Tuple[int, int]]] :param requests: HttpRequests object """ self.multipart_mixed_info = ( requests, kwargs.pop("policies", []), - kwargs.pop("boundary", []), + kwargs.pop("boundary", None), + kwargs.pop("changesets", []) ) def prepare_multipart_body(self): @@ -398,21 +435,27 @@ def prepare_multipart_body(self): if not self.multipart_mixed_info: return - requests = self.multipart_mixed_info[0] # type: List[HttpRequest] boundary = self.multipart_mixed_info[2] # type: Optional[str] + requests = self.multipart_mixed_info[0] # type: List[HttpRequest] + start_changeset, end_changeset = self._calculate_changesets() # Update the main request with the body main_message = Message() main_message.add_header("Content-Type", "multipart/mixed") if boundary: main_message.set_boundary(boundary) - for i, req in enumerate(requests): - part_message = Message() - part_message.add_header("Content-Type", "application/http") - part_message.add_header("Content-Transfer-Encoding", "binary") - part_message.add_header("Content-ID", str(i)) - part_message.set_payload(req.serialize()) - main_message.attach(part_message) + + working_message = main_message + for index, request in enumerate(requests): + if index in start_changeset: + working_message = Message() + working_message.add_header("Content-Type", "multipart/mixed") + if start_changeset[index]: + working_message.set_boundary(start_changeset[index]) + self._add_message_part(index, working_message, request) + if index in end_changeset: + main_message.attach(working_message) + working_message = main_message try: from email.policy import HTTP @@ -482,6 +525,32 @@ def text(self, encoding=None): encoding = "utf-8-sig" return self.body().decode(encoding) + def _decode_parts(self, message, http_response_type, requests): + # type: (Message, Type[_HttpResponseBase], List[HttpRequest]) -> Iterator[HttpResponse] + """Rebuild an HTTP response from pure string.""" + responses = [] + offset = 0 + for index, raw_reponse in enumerate(message.get_payload()): + content_type = raw_reponse.get_content_type() + if content_type == "application/http": + responses.append( + _deserialize_response( + raw_reponse.get_payload(decode=True), + requests[index], + http_response_type=http_response_type, + ) + ) + elif content_type == "multipart/mixed": + # The message batch contains one or more change sets + changeset_responses = self._decode_parts(raw_reponse, http_response_type, requests[offset:]) + offset += len(changeset_responses) + responses.extend(changeset_responses) + else: + raise ValueError( + "Multipart doesn't support part other than application/http for now" + ) + return responses + def _get_raw_parts(self, http_response_type=None): # type (Optional[Type[_HttpResponseBase]]) -> Iterator[HttpResponse] """Assuming this body is multipart, return the iterator or parts. @@ -500,26 +569,9 @@ def _get_raw_parts(self, http_response_type=None): + b"\r\n\r\n" + body_as_bytes ) - message = message_parser(http_body) # type: Message - - # Rebuild an HTTP response from pure string requests = self.request.multipart_mixed_info[0] # type: List[HttpRequest] - responses = [] - for request, raw_reponse in zip(requests, message.get_payload()): - if raw_reponse.get_content_type() == "application/http": - responses.append( - _deserialize_response( - raw_reponse.get_payload(decode=True), - request, - http_response_type=http_response_type, - ) - ) - else: - raise ValueError( - "Multipart doesn't support part other than application/http for now" - ) - return responses + return self._decode_parts(message, http_response_type, requests) class HttpResponse(_HttpResponseBase): # pylint: disable=abstract-method diff --git a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py index 7c58f39a39af..5a6ec3380da2 100644 --- a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py @@ -18,6 +18,28 @@ import pytest +# transport = mock.MagicMock(spec=AsyncHttpTransport) +# MagicMock support async cxt manager only after 3.8 +# https://github.com/python/cpython/pull/9296 + +class MockAsyncHttpTransport(AsyncHttpTransport): + async def __aenter__(self): return self + async def __aexit__(self, *args): pass + async def open(self): pass + async def close(self): pass + async def send(self, request, **kwargs): pass + + +class MockResponse(AsyncHttpResponse): + def __init__(self, request, body, content_type): + super(MockResponse, self).__init__(request, None) + self._body = body + self.content_type = content_type + + def body(self): + return self._body + + @pytest.mark.asyncio async def test_basic_options_aiohttp(): @@ -31,18 +53,6 @@ async def test_basic_options_aiohttp(): @pytest.mark.asyncio async def test_multipart_send(): - - # transport = mock.MagicMock(spec=AsyncHttpTransport) - # MagicMock support async cxt manager only after 3.8 - # https://github.com/python/cpython/pull/9296 - - class MockAsyncHttpTransport(AsyncHttpTransport): - async def __aenter__(self): return self - async def __aexit__(self, *args): pass - async def open(self): pass - async def close(self): pass - async def send(self, request, **kwargs): pass - transport = MockAsyncHttpTransport() class RequestPolicy(object): @@ -89,18 +99,6 @@ async def on_request(self, request): @pytest.mark.asyncio async def test_multipart_send_with_context(): - - # transport = mock.MagicMock(spec=AsyncHttpTransport) - # MagicMock support async cxt manager only after 3.8 - # https://github.com/python/cpython/pull/9296 - - class MockAsyncHttpTransport(AsyncHttpTransport): - async def __aenter__(self): return self - async def __aexit__(self, *args): pass - async def open(self): pass - async def close(self): pass - async def send(self, request, **kwargs): pass - transport = MockAsyncHttpTransport() header_policy = HeadersPolicy() @@ -116,7 +114,7 @@ async def on_request(self, request): request.set_multipart_mixed( req0, req1, - policies=[header_policy, RequestPolicy()], + policies=[RequestPolicy(), header_policy], boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" # Fix it so test are deterministic ) @@ -149,16 +147,303 @@ async def on_request(self, request): @pytest.mark.asyncio -async def test_multipart_receive(): +async def test_multipart_send_with_one_changeset(): + transport = MockAsyncHttpTransport() + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_multiple_changesets(): + transport = MockAsyncHttpTransport() + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[ + (0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), + (2, 3, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'DELETE /container3/blob3 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_combination_changeset_first(): + transport = MockAsyncHttpTransport() + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_combination_changeset_last(): + transport = MockAsyncHttpTransport() + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(1, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_combination_changeset_middle(): + transport = MockAsyncHttpTransport() + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] - class MockResponse(AsyncHttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(1, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) - def body(self): - return self._body + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_changeset_overlap(): + transport = MockAsyncHttpTransport() + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[ + (0, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), + (1, 2, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] + ) + + async with AsyncPipeline(transport) as pipeline: + with pytest.raises(ValueError) as e: + await pipeline.run(request) + assert str(e.value) == "Changesets must not overlap." + + +@pytest.mark.asyncio +async def test_multipart_receive(): class ResponsePolicy(object): def on_response(self, request, response): @@ -237,16 +522,6 @@ async def test_multipart_receive_with_bom(): request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed(req0) - - class MockResponse(AsyncHttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - - def body(self): - return self._body - body_as_bytes = ( b"--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\n" b"Content-Type: application/http\n" @@ -287,16 +562,6 @@ async def test_recursive_multipart_receive(): request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed(req0) - - class MockResponse(AsyncHttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - - def body(self): - return self._body - internal_body_as_str = ( "--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n" "Content-Type: application/http\r\n" diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index e9a8a879693a..7b664843ac02 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -258,6 +258,358 @@ def test_multipart_send_with_context(): ) +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_one_changeset(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_multiple_changesets(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[ + (0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), + (2, 3, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'DELETE /container3/blob3 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_combination_changeset_first(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_combination_changeset_last(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(1, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_combination_changeset_middle(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[(1, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_changeset_overlap(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + changesets=[ + (0, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), + (1, 2, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] + ) + + with Pipeline(transport) as pipeline: + with pytest.raises(ValueError) as e: + pipeline.run(request) + + assert str(e.value) == "Changesets must not overlap." + + def test_multipart_receive(): class MockResponse(HttpResponse): From 78f2003265e54e4038e93671ec70a11a8c399605 Mon Sep 17 00:00:00 2001 From: antisch Date: Wed, 25 Mar 2020 09:29:15 -0700 Subject: [PATCH 3/8] Added receive tests --- .../test_basic_transport.py | 339 ++++++++++++++++ .../azure-core/tests/test_basic_transport.py | 367 ++++++++++++++++-- 2 files changed, 677 insertions(+), 29 deletions(-) diff --git a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py index 5a6ec3380da2..5431382eebb0 100644 --- a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py @@ -515,6 +515,345 @@ async def on_response(self, request, response): assert res1.headers['x-ms-async-fun'] == 'true' +@pytest.mark.asyncio +async def test_multipart_receive_with_one_changeset(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 2 + + res0 = parts[0] + assert res0.status_code == 202 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_multiple_changesets(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + changesets=[ + (0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), + (2, 3, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] + ) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'HTTP/1.1 409\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 4 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + assert parts[3].status_code == 409 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_combination_changeset_first(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + *requests, + changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + ) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_combination_changeset_middle(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_combination_changeset_last(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + @pytest.mark.asyncio async def test_multipart_receive_with_bom(): diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 7b664843ac02..930dff97f9b3 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -21,6 +21,16 @@ import pytest +class MockResponse(HttpResponse): + def __init__(self, request, body, content_type): + super(MockResponse, self).__init__(request, None) + self._body = body + self.content_type = content_type + + def body(self): + return self._body + + @pytest.mark.skipif(sys.version_info < (3, 6), reason="Multipart serialization not supported on 2.7 + dict order not deterministic on 3.5") def test_http_request_serialization(): # Method + Url @@ -612,15 +622,6 @@ def test_multipart_send_with_changeset_overlap(): def test_multipart_receive(): - class MockResponse(HttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - - def body(self): - return self._body - class ResponsePolicy(object): def on_response(self, request, response): # type: (PipelineRequest, PipelineResponse) -> None @@ -681,22 +682,339 @@ def on_response(self, request, response): assert res1.status_code == 404 assert res1.headers['x-ms-fun'] == 'true' -def test_multipart_receive_with_bom(): - req0 = HttpRequest("DELETE", "/container0/blob0") +def test_multipart_receive_with_one_changeset(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ] request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(req0) + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 2 + + res0 = parts[0] + assert res0.status_code == 202 + + +def test_multipart_receive_with_multiple_changesets(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'HTTP/1.1 409\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) - class MockResponse(HttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) - def body(self): - return self._body + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 4 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + assert parts[3].status_code == 409 + + +def test_multipart_receive_with_combination_changeset_first(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +def test_multipart_receive_with_combination_changeset_middle(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +def test_multipart_receive_with_combination_changeset_last(): + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ] + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(*requests) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +def test_multipart_receive_with_bom(): + + req0 = HttpRequest("DELETE", "/container0/blob0") + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(req0) body_as_bytes = ( b"--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\n" b"Content-Type: application/http\n" @@ -734,16 +1052,6 @@ def test_recursive_multipart_receive(): request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed(req0) - - class MockResponse(HttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - - def body(self): - return self._body - internal_body_as_str = ( "--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n" "Content-Type: application/http\r\n" @@ -786,6 +1094,7 @@ def body(self): internal_response0 = internal_response[0] assert internal_response0.status_code == 400 + def test_close_unopened_transport(): transport = RequestsTransport() transport.close() From 96f56cb2aa51afcd2966a72ed2ebc3be6ac25b42 Mon Sep 17 00:00:00 2001 From: antisch Date: Wed, 25 Mar 2020 10:17:31 -0700 Subject: [PATCH 4/8] Fix pylint + mypy --- .../azure/core/pipeline/transport/_base.py | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py index 6dd179e0f72f..c047f1e61563 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py @@ -37,7 +37,6 @@ import os import time import copy -import uuid try: binary_type = str @@ -61,6 +60,7 @@ Optional, Tuple, Iterator, + Type ) from six.moves.http_client import HTTPConnection, HTTPResponse as _HTTPResponse @@ -139,6 +139,37 @@ def _urljoin(base_url, stub_url): return parsed.geturl() +def _calculate_changesets(changesets): + # type: (List[Union[Tuple[int, int, str], Tuple[int, int]]]) -> Tuple[Dict[int, Optional[str]], List[int]] + """Convert the changeset ranges into start/end indexes.""" + start = {} + end = [] + validate_indexes = [] + boundary = None # type: Optional[str] + for changeset in changesets: + try: + start_index, end_index, boundary = cast('Tuple[int, int, str]', changeset) + except ValueError: + start_index, end_index = cast('Tuple[int, int]', changeset) + validate_indexes.extend(list(range(start_index, end_index + 1))) + start[start_index] = boundary + end.append(end_index) + if len(validate_indexes) != len(set(validate_indexes)): + raise ValueError("Changesets must not overlap.") + return start, end + + +def _add_message_part(index, message, request): + # type: (int, Message, HttpRequest) -> None + """Add each request to the message batch.""" + part_message = Message() + part_message.add_header("Content-Type", "application/http") + part_message.add_header("Content-Transfer-Encoding", "binary") + part_message.add_header("Content-ID", str(index)) + part_message.set_payload(request.serialize()) + message.attach(part_message) + + class _HTTPSerializer(HTTPConnection, object): """Hacking the stdlib HTTPConnection to serialize HTTP request as strings. """ @@ -272,36 +303,6 @@ def _format_data(data): pass return (data_name, data, "application/octet-stream") return (None, cast(str, data)) - - def _calculate_changesets(self): - # type: () -> Tuple[Dict[int, Optional[str]], List[int]] - """Convert the changeset ranges into start/end indexes.""" - changesets = self.multipart_mixed_info[3] # type: List[Union[Tuple[int, int, str], Tuple[int, int]]] - start = {} - end = [] - validate_indexes = [] - for changeset in changesets: - try: - start_index, end_index, boundary = changeset - except ValueError: - start_index, end_index = changeset - boundary = None - validate_indexes.extend(list(range(start_index, end_index + 1))) - start[start_index] = boundary - end.append(end_index) - if len(validate_indexes) != len(set(validate_indexes)): - raise ValueError("Changesets must not overlap.") - return start, end - - def _add_message_part(self, index, message, request): - # type: (int, Message, HttpRequest) -> None - """Add each request to the message batch.""" - part_message = Message() - part_message.add_header("Content-Type", "application/http") - part_message.add_header("Content-Transfer-Encoding", "binary") - part_message.add_header("Content-ID", str(index)) - part_message.set_payload(request.serialize()) - message.attach(part_message) def format_parameters(self, params): # type: (Dict[str, str]) -> None @@ -437,7 +438,7 @@ def prepare_multipart_body(self): boundary = self.multipart_mixed_info[2] # type: Optional[str] requests = self.multipart_mixed_info[0] # type: List[HttpRequest] - start_changeset, end_changeset = self._calculate_changesets() + start_changeset, end_changeset = _calculate_changesets(self.multipart_mixed_info[3]) # Update the main request with the body main_message = Message() @@ -450,9 +451,10 @@ def prepare_multipart_body(self): if index in start_changeset: working_message = Message() working_message.add_header("Content-Type", "multipart/mixed") - if start_changeset[index]: - working_message.set_boundary(start_changeset[index]) - self._add_message_part(index, working_message, request) + changeset_boundary = start_changeset[index] + if changeset_boundary: + working_message.set_boundary(changeset_boundary) + _add_message_part(index, working_message, request) if index in end_changeset: main_message.attach(working_message) working_message = main_message @@ -526,7 +528,7 @@ def text(self, encoding=None): return self.body().decode(encoding) def _decode_parts(self, message, http_response_type, requests): - # type: (Message, Type[_HttpResponseBase], List[HttpRequest]) -> Iterator[HttpResponse] + # type: (Message, Type[_HttpResponseBase], List[HttpRequest]) -> List[HttpResponse] """Rebuild an HTTP response from pure string.""" responses = [] offset = 0 From ad3b6098fe37e71d179c8eb94c7802a316d7dd4b Mon Sep 17 00:00:00 2001 From: antisch Date: Tue, 21 Apr 2020 14:39:37 -0700 Subject: [PATCH 5/8] Update to use recursive requests --- .../azure-core/azure/core/pipeline/_base.py | 3 + .../azure/core/pipeline/_base_async.py | 3 + .../azure/core/pipeline/transport/_base.py | 83 ++++-------- .../test_basic_transport.py | 111 +++++++--------- .../azure-core/tests/test_basic_transport.py | 124 ++++++++---------- 5 files changed, 134 insertions(+), 190 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/_base.py b/sdk/core/azure-core/azure/core/pipeline/_base.py index 9c92eaea76b0..f442fc88dedb 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/_base.py @@ -175,6 +175,9 @@ def _prepare_multipart_mixed_request(request): import concurrent.futures def prepare_requests(req): + if req.multipart_mixed_info: + # Recursively update changeset "sub requests" + Pipeline._prepare_multipart_mixed_request(req) context = PipelineContext(None, **pipeline_options) pipeline_request = PipelineRequest(req, context) for policy in policies: diff --git a/sdk/core/azure-core/azure/core/pipeline/_base_async.py b/sdk/core/azure-core/azure/core/pipeline/_base_async.py index dcf0cdd9b6f3..4ce843176932 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_base_async.py +++ b/sdk/core/azure-core/azure/core/pipeline/_base_async.py @@ -183,6 +183,9 @@ async def _prepare_multipart_mixed_request(self, request): pipeline_options = multipart_mixed_info[3] # type: Dict[str, Any] async def prepare_requests(req): + if req.multipart_mixed_info: + # Recursively update changeset "sub requests" + await self._prepare_multipart_mixed_request(req) context = PipelineContext(None, **pipeline_options) pipeline_request = PipelineRequest(req, context) for policy in policies: diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py index 78d9bfee68d9..f6ddbb507763 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py @@ -60,7 +60,6 @@ Optional, Tuple, Iterator, - Type ) from six.moves.http_client import HTTPConnection, HTTPResponse as _HTTPResponse @@ -139,37 +138,6 @@ def _urljoin(base_url, stub_url): return parsed.geturl() -def _calculate_changesets(changesets): - # type: (List[Union[Tuple[int, int, str], Tuple[int, int]]]) -> Tuple[Dict[int, Optional[str]], List[int]] - """Convert the changeset ranges into start/end indexes.""" - start = {} - end = [] - validate_indexes = [] - boundary = None # type: Optional[str] - for changeset in changesets: - try: - start_index, end_index, boundary = cast('Tuple[int, int, str]', changeset) - except ValueError: - start_index, end_index = cast('Tuple[int, int]', changeset) - validate_indexes.extend(list(range(start_index, end_index + 1))) - start[start_index] = boundary - end.append(end_index) - if len(validate_indexes) != len(set(validate_indexes)): - raise ValueError("Changesets must not overlap.") - return start, end - - -def _add_message_part(index, message, request): - # type: (int, Message, HttpRequest) -> None - """Add each request to the message batch.""" - part_message = Message() - part_message.add_header("Content-Type", "application/http") - part_message.add_header("Content-Transfer-Encoding", "binary") - part_message.add_header("Content-ID", str(index)) - part_message.set_payload(request.serialize()) - message.attach(part_message) - - class _HTTPSerializer(HTTPConnection, object): """Hacking the stdlib HTTPConnection to serialize HTTP request as strings. """ @@ -411,36 +379,33 @@ def set_multipart_mixed(self, *requests, **kwargs): :keyword list[SansIOHTTPPolicy] policies: SansIOPolicy to apply at preparation time :keyword str boundary: Optional boundary - :keyword changesets: Optional list of tuples marking the change sets within the request list. - Each tuple should contain two integers: the start and inclusive-end indexes of each change set - within the request list. Optionally, the tuple can also contain a changeset boundary string. - Any request that fall outside the changeset ranges will be added to the batch normally. - Change set ranges must not overlap. - :paramtype changesets: List[Union[Tuple[int, int, str], Tuple[int, int]]] :param requests: HttpRequests object """ self.multipart_mixed_info = ( requests, kwargs.pop("policies", []), - kwargs.pop("boundary", []), + kwargs.pop("boundary", None), kwargs ) - def prepare_multipart_body(self): - # type: () -> None + def prepare_multipart_body(self, content_index=0): + # type: (int) -> int """Will prepare the body of this request according to the multipart information. This call assumes the on_request policies have been applied already in their correct context (sync/async) Does nothing if "set_multipart_mixed" was never called. + + :param int content_index: The current index of parts within the batch message. + :returns: The updated index after all parts in this request have been added. + :rtype: int """ if not self.multipart_mixed_info: - return + return 0 - boundary = self.multipart_mixed_info[2] # type: Optional[str] requests = self.multipart_mixed_info[0] # type: List[HttpRequest] - start_changeset, end_changeset = _calculate_changesets(self.multipart_mixed_info[3]) + boundary = self.multipart_mixed_info[2] # type: Optional[str] # Update the main request with the body main_message = Message() @@ -448,18 +413,23 @@ def prepare_multipart_body(self): if boundary: main_message.set_boundary(boundary) - working_message = main_message - for index, request in enumerate(requests): - if index in start_changeset: - working_message = Message() - working_message.add_header("Content-Type", "multipart/mixed") - changeset_boundary = start_changeset[index] - if changeset_boundary: - working_message.set_boundary(changeset_boundary) - _add_message_part(index, working_message, request) - if index in end_changeset: - main_message.attach(working_message) - working_message = main_message + for req in requests: + part_message = Message() + if req.multipart_mixed_info: + content_index = req.prepare_multipart_body(content_index=content_index) + part_message.add_header("Content-Type", req.headers['Content-Type']) + payload = req.serialize() + # We need to remove the ~HTTP/1.1 prefix along with the added content-length + payload = payload[payload.index(b'--'):] + + else: + part_message.add_header("Content-Type", "application/http") + part_message.add_header("Content-Transfer-Encoding", "binary") + part_message.add_header("Content-ID", str(content_index)) + payload = req.serialize() + content_index += 1 + part_message.set_payload(payload) + main_message.attach(part_message) try: from email.policy import HTTP @@ -479,6 +449,7 @@ def prepare_multipart_body(self): self.headers["Content-Type"] = ( "multipart/mixed; boundary=" + main_message.get_boundary() ) + return content_index def serialize(self): # type: () -> bytes diff --git a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py index 40a7d45ee2ec..013ecd3e69d1 100644 --- a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py @@ -155,12 +155,16 @@ async def test_multipart_send_with_one_changeset(): HttpRequest("DELETE", "/container0/blob0"), HttpRequest("DELETE", "/container1/blob1") ] + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + *requests, + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + changeset, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" ) async with AsyncPipeline(transport) as pipeline: @@ -168,7 +172,7 @@ async def test_multipart_send_with_one_changeset(): assert request.body == ( b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -195,20 +199,24 @@ async def test_multipart_send_with_one_changeset(): @pytest.mark.asyncio async def test_multipart_send_with_multiple_changesets(): transport = MockAsyncHttpTransport() - requests = [ + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), HttpRequest("DELETE", "/container1/blob1"), + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( HttpRequest("DELETE", "/container2/blob2"), - HttpRequest("DELETE", "/container3/blob3") - ] + HttpRequest("DELETE", "/container3/blob3"), + boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, + changeset1, + changeset2, boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[ - (0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), - (2, 3, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] ) async with AsyncPipeline(transport) as pipeline: @@ -216,7 +224,7 @@ async def test_multipart_send_with_multiple_changesets(): assert request.body == ( b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -237,7 +245,7 @@ async def test_multipart_send_with_multiple_changesets(): b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' b'\r\n' b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' b'\r\n' b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' b'Content-Type: application/http\r\n' @@ -264,17 +272,18 @@ async def test_multipart_send_with_multiple_changesets(): @pytest.mark.asyncio async def test_multipart_send_with_combination_changeset_first(): transport = MockAsyncHttpTransport() - requests = [ + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + changeset, + HttpRequest("DELETE", "/container2/blob2"), + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" ) async with AsyncPipeline(transport) as pipeline: @@ -282,7 +291,7 @@ async def test_multipart_send_with_combination_changeset_first(): assert request.body == ( b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -317,17 +326,17 @@ async def test_multipart_send_with_combination_changeset_first(): @pytest.mark.asyncio async def test_multipart_send_with_combination_changeset_last(): transport = MockAsyncHttpTransport() - requests = [ - HttpRequest("DELETE", "/container0/blob0"), + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - + HttpRequest("DELETE", "/container2/blob2"), + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(1, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + HttpRequest("DELETE", "/container0/blob0"), + changeset, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" ) async with AsyncPipeline(transport) as pipeline: @@ -343,7 +352,7 @@ async def test_multipart_send_with_combination_changeset_last(): b'\r\n' b'\r\n' b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -370,17 +379,17 @@ async def test_multipart_send_with_combination_changeset_last(): @pytest.mark.asyncio async def test_multipart_send_with_combination_changeset_middle(): transport = MockAsyncHttpTransport() - requests = [ - HttpRequest("DELETE", "/container0/blob0"), + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(1, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2"), + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" ) async with AsyncPipeline(transport) as pipeline: @@ -396,7 +405,7 @@ async def test_multipart_send_with_combination_changeset_middle(): b'\r\n' b'\r\n' b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -420,30 +429,6 @@ async def test_multipart_send_with_combination_changeset_middle(): ) -@pytest.mark.asyncio -async def test_multipart_send_with_changeset_overlap(): - transport = MockAsyncHttpTransport() - requests = [ - HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - - request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed( - *requests, - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[ - (0, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), - (1, 2, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] - ) - - async with AsyncPipeline(transport) as pipeline: - with pytest.raises(ValueError) as e: - await pipeline.run(request) - assert str(e.value) == "Changesets must not overlap." - - @pytest.mark.asyncio async def test_multipart_receive(): diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 40fbb6ecd9fa..b2772ee49d15 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -282,12 +282,17 @@ def test_multipart_send_with_one_changeset(): HttpRequest("DELETE", "/container1/blob1") ] - request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed( + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( *requests, policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + changeset, boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] ) with Pipeline(transport) as pipeline: @@ -295,7 +300,7 @@ def test_multipart_send_with_one_changeset(): assert request.body == ( b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -330,21 +335,27 @@ def test_multipart_send_with_multiple_changesets(): 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' }) - requests = [ + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), HttpRequest("DELETE", "/container1/blob1"), + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( HttpRequest("DELETE", "/container2/blob2"), - HttpRequest("DELETE", "/container3/blob3") - ] + HttpRequest("DELETE", "/container3/blob3"), + policies=[header_policy], + boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, + changeset1, + changeset2, policies=[header_policy], boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[ - (0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), - (2, 3, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] ) with Pipeline(transport) as pipeline: @@ -352,7 +363,7 @@ def test_multipart_send_with_multiple_changesets(): assert request.body == ( b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -375,7 +386,7 @@ def test_multipart_send_with_multiple_changesets(): b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' b'\r\n' b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' b'\r\n' b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' b'Content-Type: application/http\r\n' @@ -410,18 +421,19 @@ def test_multipart_send_with_combination_changeset_first(): 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' }) - requests = [ + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, + changeset, + HttpRequest("DELETE", "/container2/blob2"), policies=[header_policy], - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" ) with Pipeline(transport) as pipeline: @@ -429,7 +441,7 @@ def test_multipart_send_with_combination_changeset_first(): assert request.body == ( b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -472,18 +484,19 @@ def test_multipart_send_with_combination_changeset_last(): 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' }) - requests = [ - HttpRequest("DELETE", "/container0/blob0"), + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - + HttpRequest("DELETE", "/container2/blob2"), + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, + HttpRequest("DELETE", "/container0/blob0"), + changeset, policies=[header_policy], - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(1, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" ) with Pipeline(transport) as pipeline: @@ -500,7 +513,7 @@ def test_multipart_send_with_combination_changeset_last(): b'\r\n' b'\r\n' b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -534,18 +547,19 @@ def test_multipart_send_with_combination_changeset_middle(): 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' }) - requests = [ - HttpRequest("DELETE", "/container0/blob0"), + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed( - *requests, + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2"), policies=[header_policy], - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[(1, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" ) with Pipeline(transport) as pipeline: @@ -562,7 +576,7 @@ def test_multipart_send_with_combination_changeset_middle(): b'\r\n' b'\r\n' b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' - b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'\r\n' b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' b'Content-Type: application/http\r\n' @@ -588,38 +602,6 @@ def test_multipart_send_with_combination_changeset_middle(): ) -@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") -def test_multipart_send_with_changeset_overlap(): - - transport = mock.MagicMock(spec=HttpTransport) - - header_policy = HeadersPolicy({ - 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' - }) - - requests = [ - HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] - - request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed( - *requests, - policies=[header_policy], - boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", - changesets=[ - (0, 2, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), - (1, 2, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] - ) - - with Pipeline(transport) as pipeline: - with pytest.raises(ValueError) as e: - pipeline.run(request) - - assert str(e.value) == "Changesets must not overlap." - - def test_multipart_receive(): class ResponsePolicy(object): From a9e05afaaa63675ce08d727ed544c86541c71ee5 Mon Sep 17 00:00:00 2001 From: antisch Date: Tue, 21 Apr 2020 15:09:56 -0700 Subject: [PATCH 6/8] CI fix --- sdk/core/azure-core/azure/core/pipeline/transport/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py index f6ddbb507763..68998df9517e 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py @@ -60,6 +60,7 @@ Optional, Tuple, Iterator, + Type ) from six.moves.http_client import HTTPConnection, HTTPResponse as _HTTPResponse @@ -421,7 +422,6 @@ def prepare_multipart_body(self, content_index=0): payload = req.serialize() # We need to remove the ~HTTP/1.1 prefix along with the added content-length payload = payload[payload.index(b'--'):] - else: part_message.add_header("Content-Type", "application/http") part_message.add_header("Content-Transfer-Encoding", "binary") From 68c79b901847f090d5efca5a3e0d27a554413fbf Mon Sep 17 00:00:00 2001 From: antisch Date: Wed, 22 Apr 2020 06:42:09 -0700 Subject: [PATCH 7/8] Update changeset response decoding --- .../azure/core/pipeline/transport/_base.py | 5 +- .../test_basic_transport.py | 60 +++++++++---------- .../azure-core/tests/test_basic_transport.py | 52 +++++++++------- 3 files changed, 61 insertions(+), 56 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py index 68998df9517e..04dd1311a8ad 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py @@ -504,7 +504,6 @@ def _decode_parts(self, message, http_response_type, requests): # type: (Message, Type[_HttpResponseBase], List[HttpRequest]) -> List[HttpResponse] """Rebuild an HTTP response from pure string.""" responses = [] - offset = 0 for index, raw_reponse in enumerate(message.get_payload()): content_type = raw_reponse.get_content_type() if content_type == "application/http": @@ -517,8 +516,8 @@ def _decode_parts(self, message, http_response_type, requests): ) elif content_type == "multipart/mixed": # The message batch contains one or more change sets - changeset_responses = self._decode_parts(raw_reponse, http_response_type, requests[offset:]) - offset += len(changeset_responses) + changeset_requests = requests[index].multipart_mixed_info[0] + changeset_responses = self._decode_parts(raw_reponse, http_response_type, changeset_requests) responses.extend(changeset_responses) else: raise ValueError( diff --git a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py index 013ecd3e69d1..22810b8afffd 100644 --- a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py @@ -504,14 +504,14 @@ async def on_response(self, request, response): @pytest.mark.asyncio async def test_multipart_receive_with_one_changeset(): - - requests = [ + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), HttpRequest("DELETE", "/container1/blob1") - ] + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed(changeset) body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' @@ -559,20 +559,19 @@ async def test_multipart_receive_with_one_changeset(): @pytest.mark.asyncio async def test_multipart_receive_with_multiple_changesets(): - requests = [ + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container1/blob1") + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( HttpRequest("DELETE", "/container2/blob2"), HttpRequest("DELETE", "/container3/blob3") - ] + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed( - *requests, - changesets=[ - (0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"), - (2, 3, "changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314")] - ) + request.set_multipart_mixed(changeset1, changeset2) body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' @@ -646,17 +645,14 @@ async def test_multipart_receive_with_multiple_changesets(): @pytest.mark.asyncio async def test_multipart_receive_with_combination_changeset_first(): - requests = [ + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] + HttpRequest("DELETE", "/container1/blob1") + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed( - *requests, - changesets=[(0, 1, "changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525")] - ) + request.set_multipart_mixed(changeset, HttpRequest("DELETE", "/container2/blob2")) body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' @@ -714,14 +710,15 @@ async def test_multipart_receive_with_combination_changeset_first(): @pytest.mark.asyncio async def test_multipart_receive_with_combination_changeset_middle(): - requests = [ - HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed(HttpRequest("DELETE", "/container1/blob1")) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2") + ) body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: application/http\r\n' @@ -779,14 +776,15 @@ async def test_multipart_receive_with_combination_changeset_middle(): @pytest.mark.asyncio async def test_multipart_receive_with_combination_changeset_last(): - requests = [ - HttpRequest("DELETE", "/container0/blob0"), + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container1/blob1"), HttpRequest("DELETE", "/container2/blob2") - ] + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed(HttpRequest("DELETE", "/container0/blob0"), changeset) + body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: application/http\r\n' diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index b2772ee49d15..d3d13eac2089 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -667,13 +667,15 @@ def on_response(self, request, response): def test_multipart_receive_with_one_changeset(): - requests = [ + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), HttpRequest("DELETE", "/container1/blob1") - ] + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed(changeset) + body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' @@ -720,15 +722,19 @@ def test_multipart_receive_with_one_changeset(): def test_multipart_receive_with_multiple_changesets(): - requests = [ + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container1/blob1") + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( HttpRequest("DELETE", "/container2/blob2"), HttpRequest("DELETE", "/container3/blob3") - ] + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed(changeset1, changeset2) body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' @@ -801,14 +807,14 @@ def test_multipart_receive_with_multiple_changesets(): def test_multipart_receive_with_combination_changeset_first(): - requests = [ + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] + HttpRequest("DELETE", "/container1/blob1") + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed(changeset, HttpRequest("DELETE", "/container2/blob2")) body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' @@ -865,14 +871,15 @@ def test_multipart_receive_with_combination_changeset_first(): def test_multipart_receive_with_combination_changeset_middle(): - requests = [ - HttpRequest("DELETE", "/container0/blob0"), - HttpRequest("DELETE", "/container1/blob1"), - HttpRequest("DELETE", "/container2/blob2") - ] + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed(HttpRequest("DELETE", "/container1/blob1")) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2") + ) body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: application/http\r\n' @@ -929,14 +936,15 @@ def test_multipart_receive_with_combination_changeset_middle(): def test_multipart_receive_with_combination_changeset_last(): - requests = [ - HttpRequest("DELETE", "/container0/blob0"), + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( HttpRequest("DELETE", "/container1/blob1"), HttpRequest("DELETE", "/container2/blob2") - ] + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(*requests) + request.set_multipart_mixed(HttpRequest("DELETE", "/container0/blob0"), changeset) + body_as_bytes = ( b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' b'Content-Type: application/http\r\n' From 68c2c172b76c72fe5128a572ffad65cd362be678 Mon Sep 17 00:00:00 2001 From: antisch Date: Wed, 22 Apr 2020 08:53:46 -0700 Subject: [PATCH 8/8] Make mypy happy --- sdk/core/azure-core/azure/core/pipeline/transport/_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py index 04dd1311a8ad..c2719b97464c 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py @@ -514,9 +514,9 @@ def _decode_parts(self, message, http_response_type, requests): http_response_type=http_response_type, ) ) - elif content_type == "multipart/mixed": + elif content_type == "multipart/mixed" and requests[index].multipart_mixed_info: # The message batch contains one or more change sets - changeset_requests = requests[index].multipart_mixed_info[0] + changeset_requests = requests[index].multipart_mixed_info[0] # type: ignore changeset_responses = self._decode_parts(raw_reponse, http_response_type, changeset_requests) responses.extend(changeset_responses) else: