From 505c5b95814d5357f7f3a9f5306bf6f5730fbd92 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Mon, 14 Oct 2024 12:10:36 +0300 Subject: [PATCH] sync sources for https://github.com/microsoft/kiota-http-python/pull/434 --- packages/http/httpx/CHANGELOG.md | 6 ++ .../httpx/kiota_http/httpx_request_adapter.py | 30 ++++--- .../kiota_http/middleware/redirect_handler.py | 6 +- .../kiota_http/middleware/retry_handler.py | 8 +- .../middleware/url_replace_handler.py | 4 +- .../middleware/user_agent_handler.py | 1 - packages/http/httpx/tests/conftest.py | 16 ++-- .../tests/helpers/mock_async_transport.py | 12 ++- .../middleware_tests/test_base_middleware.py | 1 + .../test_headers_inspection_handler.py | 27 +++---- .../test_parameters_name_decoding_handler.py | 52 +++++++----- .../middleware_tests/test_redirect_handler.py | 79 +++++++++++-------- .../middleware_tests/test_retry_handler.py | 69 +++++++--------- .../httpx/tests/test_httpx_request_adapter.py | 23 +++--- .../httpx/tests/test_kiota_client_factory.py | 21 +++-- 15 files changed, 199 insertions(+), 156 deletions(-) diff --git a/packages/http/httpx/CHANGELOG.md b/packages/http/httpx/CHANGELOG.md index 28182aad..93a6a94f 100644 --- a/packages/http/httpx/CHANGELOG.md +++ b/packages/http/httpx/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.4] - 2024-10-11 + +### Changed + +- Updated HTTP span attributes to comply with updated OpenTelemetry semantic conventions. [#409](https://github.com/microsoft/kiota-http-python/issues/409) + ## [1.3.3] - 2024-08-12 ### Added diff --git a/packages/http/httpx/kiota_http/httpx_request_adapter.py b/packages/http/httpx/kiota_http/httpx_request_adapter.py index a8811490..533d085d 100644 --- a/packages/http/httpx/kiota_http/httpx_request_adapter.py +++ b/packages/http/httpx/kiota_http/httpx_request_adapter.py @@ -24,7 +24,13 @@ ) from kiota_abstractions.store import BackingStoreFactory, BackingStoreFactorySingleton from opentelemetry import trace -from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, + HTTP_REQUEST_METHOD, +) +from opentelemetry.semconv.attributes.network_attributes import NETWORK_PROTOCOL_NAME +from opentelemetry.semconv.attributes.server_attributes import SERVER_ADDRESS +from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME, URL_FULL import httpx from kiota_http._exceptions import ( @@ -533,15 +539,15 @@ async def get_http_response_message( resp = await self._http_client.send(request) if not resp: raise ResponseError("Unable to get response from request") - parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code) + parent_span.set_attribute(HTTP_RESPONSE_STATUS_CODE, resp.status_code) if http_version := resp.http_version: - parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, http_version) + parent_span.set_attribute(NETWORK_PROTOCOL_NAME, http_version) if content_length := resp.headers.get("Content-Length", None): - parent_span.set_attribute(SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, content_length) + parent_span.set_attribute("http.response.body.size", content_length) if content_type := resp.headers.get("Content-Type", None): - parent_span.set_attribute("http.response_content_type", content_type) + parent_span.set_attribute("http.response.header.content-type", content_type) _get_http_resp_span.end() return await self.retry_cae_response_if_required(resp, request_info, claims) @@ -594,15 +600,15 @@ def get_request_from_request_information( raise RequestError("HTTP method must be provided") otel_attributes = { - SpanAttributes.HTTP_METHOD: method.value, + HTTP_REQUEST_METHOD: method.value, "http.port": url.port, - SpanAttributes.HTTP_HOST: url.hostname, - SpanAttributes.HTTP_SCHEME: url.scheme, - "http.uri_template": request_info.url_template, + SERVER_ADDRESS: url.hostname, + URL_SCHEME: url.scheme, + "url.uri_template": request_info.url_template, } if self.observability_options.include_euii_attributes: - otel_attributes.update({"http.uri": url.geturl()}) + otel_attributes.update({URL_FULL: url.geturl()}) request = self._http_client.build_request( method=method.value, @@ -618,10 +624,10 @@ def get_request_from_request_information( setattr(request, "options", request_options) if content_length := request.headers.get("Content-Length", None): - otel_attributes.update({SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) + otel_attributes.update({"http.request.body.size": content_length}) if content_type := request.headers.get("Content-Type", None): - otel_attributes.update({"http.request_content_type": content_type}) + otel_attributes.update({"http.request.header.content-type": content_type}) attribute_span.set_attributes(otel_attributes) # type: ignore _get_request_span.set_attributes(otel_attributes) # type: ignore _get_request_span.end() diff --git a/packages/http/httpx/kiota_http/middleware/redirect_handler.py b/packages/http/httpx/kiota_http/middleware/redirect_handler.py index 32871789..fbe02c29 100644 --- a/packages/http/httpx/kiota_http/middleware/redirect_handler.py +++ b/packages/http/httpx/kiota_http/middleware/redirect_handler.py @@ -1,7 +1,9 @@ import typing from kiota_abstractions.request_option import RequestOption -from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, +) import httpx @@ -76,7 +78,7 @@ async def send( request, f"RedirectHandler_send - redirect {len(history)}" ) response = await super().send(request, transport) - _redirect_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + _redirect_span.set_attribute(HTTP_RESPONSE_STATUS_CODE, response.status_code) redirect_location = self.get_redirect_location(response) if redirect_location and current_options.should_redirect: diff --git a/packages/http/httpx/kiota_http/middleware/retry_handler.py b/packages/http/httpx/kiota_http/middleware/retry_handler.py index e65a95a6..a0f69196 100644 --- a/packages/http/httpx/kiota_http/middleware/retry_handler.py +++ b/packages/http/httpx/kiota_http/middleware/retry_handler.py @@ -5,7 +5,9 @@ from typing import FrozenSet, Set, Type from kiota_abstractions.request_option import RequestOption -from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, +) import httpx @@ -83,7 +85,7 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport while retry_valid: start_time = time.time() response = await super().send(request, transport) - _retry_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + _retry_span.set_attribute(HTTP_RESPONSE_STATUS_CODE, response.status_code) # check that max retries has not been hit retry_valid = self.check_retry_valid(retry_count, current_options) @@ -100,7 +102,7 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport # increment the count for retries retry_count += 1 request.headers.update({'retry-attempt': f'{retry_count}'}) - _retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, retry_count) + _retry_span.set_attribute('http.request.resend_count', retry_count) continue break if response is None: diff --git a/packages/http/httpx/kiota_http/middleware/url_replace_handler.py b/packages/http/httpx/kiota_http/middleware/url_replace_handler.py index dcf03941..7b1a58e2 100644 --- a/packages/http/httpx/kiota_http/middleware/url_replace_handler.py +++ b/packages/http/httpx/kiota_http/middleware/url_replace_handler.py @@ -1,5 +1,5 @@ from kiota_abstractions.request_option import RequestOption -from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.semconv.attributes.url_attributes import (URL_FULL) import httpx @@ -41,7 +41,7 @@ async def send( url_string: str = str(request.url) url_string = self.replace_url_segment(url_string, current_options) request.url = httpx.URL(url_string) - _enable_span.set_attribute(SpanAttributes.HTTP_URL, str(request.url)) + _enable_span.set_attribute(URL_FULL, str(request.url)) response = await super().send(request, transport) _enable_span.end() return response diff --git a/packages/http/httpx/kiota_http/middleware/user_agent_handler.py b/packages/http/httpx/kiota_http/middleware/user_agent_handler.py index 2f03cf21..9ecc4779 100644 --- a/packages/http/httpx/kiota_http/middleware/user_agent_handler.py +++ b/packages/http/httpx/kiota_http/middleware/user_agent_handler.py @@ -1,5 +1,4 @@ from kiota_abstractions.request_option import RequestOption -from opentelemetry.semconv.trace import SpanAttributes from httpx import AsyncBaseTransport, Request, Response diff --git a/packages/http/httpx/tests/conftest.py b/packages/http/httpx/tests/conftest.py index a5ecb807..c008898f 100644 --- a/packages/http/httpx/tests/conftest.py +++ b/packages/http/httpx/tests/conftest.py @@ -13,10 +13,12 @@ from .helpers import MockTransport, MockErrorObject, MockResponseObject, OfficeLocation + @pytest.fixture def sample_headers(): return {"Content-Type": "application/json"} + @pytest.fixture def auth_provider(): return AnonymousAuthenticationProvider() @@ -26,6 +28,7 @@ def auth_provider(): def request_info(): return RequestInformation() + @pytest.fixture def mock_async_transport(): return MockTransport() @@ -57,6 +60,7 @@ def mock_error_500_map(): "500": Exception("Internal Server Error"), } + @pytest.fixture def mock_apierror_map(sample_headers): return { @@ -64,20 +68,24 @@ def mock_apierror_map(sample_headers): "500": APIError("Custom Internal Server Error", 500, sample_headers) } + @pytest.fixture def mock_apierror_XXX_map(sample_headers): - return {"XXX": APIError("OdataError",400, sample_headers)} - + return {"XXX": APIError("OdataError", 400, sample_headers)} + + @pytest.fixture def mock_request_adapter(sample_headers): resp = httpx.Response(json={'error': 'not found'}, status_code=404, headers=sample_headers) mock_request_adapter = AsyncMock mock_request_adapter.get_http_response_message = AsyncMock(return_value=resp) + @pytest.fixture def simple_error_response(sample_headers): return httpx.Response(json={'error': 'not found'}, status_code=404, headers=sample_headers) + @pytest.fixture def simple_success_response(sample_headers): return httpx.Response(json={'message': 'Success!'}, status_code=200, headers=sample_headers) @@ -153,9 +161,7 @@ def mock_users_response(mocker): @pytest.fixture def mock_primitive_collection_response(sample_headers): - return httpx.Response( - 200, json=[12.1, 12.2, 12.3, 12.4, 12.5], headers=sample_headers - ) + return httpx.Response(200, json=[12.1, 12.2, 12.3, 12.4, 12.5], headers=sample_headers) @pytest.fixture diff --git a/packages/http/httpx/tests/helpers/mock_async_transport.py b/packages/http/httpx/tests/helpers/mock_async_transport.py index 78f57ea2..5435f37d 100644 --- a/packages/http/httpx/tests/helpers/mock_async_transport.py +++ b/packages/http/httpx/tests/helpers/mock_async_transport.py @@ -1,5 +1,15 @@ import httpx + class MockTransport(): + async def handle_async_request(self, request): - return httpx.Response(200, request=request, content=b'Hello World', headers={"Content-Type": "application/json", "test": "test_response_header"}) \ No newline at end of file + return httpx.Response( + 200, + request=request, + content=b'Hello World', + headers={ + "Content-Type": "application/json", + "test": "test_response_header" + } + ) diff --git a/packages/http/httpx/tests/middleware_tests/test_base_middleware.py b/packages/http/httpx/tests/middleware_tests/test_base_middleware.py index 53e0bc3a..254ecabc 100644 --- a/packages/http/httpx/tests/middleware_tests/test_base_middleware.py +++ b/packages/http/httpx/tests/middleware_tests/test_base_middleware.py @@ -9,6 +9,7 @@ def test_next_is_none(): middleware = BaseMiddleware() assert middleware.next is None + def test_span_created(request_info): """Ensures the current span is returned and the parent_span is not set.""" middleware = BaseMiddleware() diff --git a/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py b/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py index 6751d86e..d47b9442 100644 --- a/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py @@ -26,47 +26,42 @@ def test_custom_config(): options = HeadersInspectionHandlerOption(inspect_request_headers=False) assert not options.inspect_request_headers - - + + def test_headers_inspection_handler_construction(): """ Ensures the Header Inspection handler instance is set. """ handler = HeadersInspectionHandler() assert handler - + + @pytest.mark.asyncio async def test_headers_inspection_handler_gets_headers(): + def request_handler(request: httpx.Request): return httpx.Response( - 200, - json={"text": "Hello, world!"}, - headers={'test_response': 'test_response_header'} + 200, json={"text": "Hello, world!"}, headers={'test_response': 'test_response_header'} ) + handler = HeadersInspectionHandler() - + # First request request = httpx.Request( - 'GET', - 'https://localhost', - headers={'test_request': 'test_request_header'} + 'GET', 'https://localhost', headers={'test_request': 'test_request_header'} ) mock_transport = httpx.MockTransport(request_handler) resp = await handler.send(request, mock_transport) assert resp.status_code == 200 assert handler.options.request_headers.try_get('test_request') == {'test_request_header'} assert handler.options.response_headers.try_get('test_response') == {'test_response_header'} - + # Second request request2 = httpx.Request( - 'GET', - 'https://localhost', - headers={'test_request_2': 'test_request_header_2'} + 'GET', 'https://localhost', headers={'test_request_2': 'test_request_header_2'} ) resp = await handler.send(request2, mock_transport) assert resp.status_code == 200 assert not handler.options.request_headers.try_get('test_request') == {'test_request_header'} assert handler.options.request_headers.try_get('test_request_2') == {'test_request_header_2'} assert handler.options.response_headers.try_get('test_response') == {'test_response_header'} - - \ No newline at end of file diff --git a/packages/http/httpx/tests/middleware_tests/test_parameters_name_decoding_handler.py b/packages/http/httpx/tests/middleware_tests/test_parameters_name_decoding_handler.py index 27829ee2..54943d05 100644 --- a/packages/http/httpx/tests/middleware_tests/test_parameters_name_decoding_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_parameters_name_decoding_handler.py @@ -5,6 +5,8 @@ from kiota_http.middleware.options import ParametersNameDecodingHandlerOption OPTION_KEY = "ParametersNameDecodingHandlerOption" + + def test_no_config(): """ Test that default values are used if no custom confguration is passed @@ -19,9 +21,7 @@ def test_custom_options(): """ Test that default configuration is overrriden if custom configuration is provided """ - options = ParametersNameDecodingHandlerOption( - enable=False, characters_to_decode=[".", "-"] - ) + options = ParametersNameDecodingHandlerOption(enable=False, characters_to_decode=[".", "-"]) handler = ParametersNameDecodingHandler(options) assert handler.options.enabled is not True @@ -35,24 +35,40 @@ async def test_decodes_query_parameter_names_only(): Test that only query parameter names are decoded """ encoded_decoded = [ - ("http://localhost?%24select=diplayName&api%2Dversion=2", "http://localhost?$select=diplayName&api-version=2"), - ("http://localhost?%24select=diplayName&api%7Eversion=2", "http://localhost?$select=diplayName&api~version=2"), - ("http://localhost?%24select=diplayName&api%2Eversion=2", "http://localhost?$select=diplayName&api.version=2"), - ("http://localhost:888?%24select=diplayName&api%2Dversion=2", "http://localhost:888?$select=diplayName&api-version=2"), - ("http://localhost", "http://localhost"), - ("https://google.com/?q=1%2b2", "https://google.com/?q=1%2b2"), - ("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"), - ("https://google.com/?q=1%2B2", "https://google.com/?q=1%2B2"), # Values are not decoded - ("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"), # Values are not decoded - ("https://google.com/?q%2D1=M%26A", "https://google.com/?q-1=M%26A"), # Values are not decoded but params are - ("https://google.com/?q%2D1&q=M%26A=M%26A", "https://google.com/?q-1&q=M%26A=M%26A"), # Values are not decoded but params are - ("https://graph.microsoft.com?%24count=true&query=%24top&created%2Din=2022-10-05&q=1%2b2&q2=M%26A&subject%2Ename=%7eWelcome&%24empty", - "https://graph.microsoft.com?$count=true&query=%24top&created-in=2022-10-05&q=1%2b2&q2=M%26A&subject.name=%7eWelcome&$empty") + ( + "http://localhost?%24select=diplayName&api%2Dversion=2", + "http://localhost?$select=diplayName&api-version=2" + ), + ( + "http://localhost?%24select=diplayName&api%7Eversion=2", + "http://localhost?$select=diplayName&api~version=2" + ), + ( + "http://localhost?%24select=diplayName&api%2Eversion=2", + "http://localhost?$select=diplayName&api.version=2" + ), + ( + "http://localhost:888?%24select=diplayName&api%2Dversion=2", + "http://localhost:888?$select=diplayName&api-version=2" + ), + ("http://localhost", "http://localhost"), + ("https://google.com/?q=1%2b2", "https://google.com/?q=1%2b2"), + ("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"), + ("https://google.com/?q=1%2B2", "https://google.com/?q=1%2B2"), # Values are not decoded + ("https://google.com/?q=M%26A", "https://google.com/?q=M%26A"), # Values are not decoded + ("https://google.com/?q%2D1=M%26A", + "https://google.com/?q-1=M%26A"), # Values are not decoded but params are + ("https://google.com/?q%2D1&q=M%26A=M%26A", + "https://google.com/?q-1&q=M%26A=M%26A"), # Values are not decoded but params are + ( + "https://graph.microsoft.com?%24count=true&query=%24top&created%2Din=2022-10-05&q=1%2b2&q2=M%26A&subject%2Ename=%7eWelcome&%24empty", + "https://graph.microsoft.com?$count=true&query=%24top&created-in=2022-10-05&q=1%2b2&q2=M%26A&subject.name=%7eWelcome&$empty" + ) ] - + def request_handler(request: httpx.Request): return httpx.Response(200, json={"text": "Hello, world!"}) - + handler = ParametersNameDecodingHandler() for encoded, decoded in encoded_decoded: request = httpx.Request('GET', encoded) diff --git a/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py b/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py index 80ac8fb0..1f7308b1 100644 --- a/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py @@ -79,14 +79,15 @@ def test_is_not_https_redirect(mock_redirect_handler): url = httpx.URL(BASE_URL) location = httpx.URL("http://www.example.com") assert not mock_redirect_handler.is_https_redirect(url, location) - + + @pytest.mark.asyncio async def test_ok_response_not_redirected(): """Test that a 200 response is not redirected""" + def request_handler(request: httpx.Request): - return httpx.Response( - 200, - ) + return httpx.Response(200, ) + handler = RedirectHandler() request = httpx.Request( 'GET', @@ -97,19 +98,20 @@ def request_handler(request: httpx.Request): assert resp.status_code == 200 assert resp.request assert resp.request == request - + + @pytest.mark.asyncio async def test_redirects_valid(): """Test that a valid response is redirected""" + def request_handler(request: httpx.Request): if request.url == REDIRECT_URL: - return httpx.Response( - 200, - ) + return httpx.Response(200, ) return httpx.Response( MOVED_PERMANENTLY, headers={LOCATION_HEADER: REDIRECT_URL}, ) + handler = RedirectHandler() request = httpx.Request( 'GET', @@ -122,19 +124,20 @@ def request_handler(request: httpx.Request): assert resp.request.method == request.method assert resp.request.url == REDIRECT_URL + @pytest.mark.asyncio async def test_redirect_to_different_host_removes_auth_header(): """Test that if a request is redirected to a different host, the Authorization header is removed""" + def request_handler(request: httpx.Request): if request.url == "https://httpbin.org": - return httpx.Response( - 200, - ) + return httpx.Response(200, ) return httpx.Response( FOUND, headers={LOCATION_HEADER: "https://httpbin.org"}, ) + handler = RedirectHandler() request = httpx.Request( 'GET', @@ -149,42 +152,44 @@ def request_handler(request: httpx.Request): assert resp.request.url == "https://httpbin.org" assert AUTHORIZATION_HEADER not in resp.request.headers + @pytest.mark.asyncio async def test_redirect_on_scheme_change_disabled(): """Test that a request is not redirected if the scheme changes and allow_redirect_on_scheme_change is set to False""" + def request_handler(request: httpx.Request): - if request.url == "http://example.com": - return httpx.Response( - 200, - ) + if request.url == "http://example.com": + return httpx.Response(200, ) return httpx.Response( TEMPORARY_REDIRECT, headers={LOCATION_HEADER: "http://example.com"}, ) + handler = RedirectHandler() request = httpx.Request( 'GET', BASE_URL, ) mock_transport = httpx.MockTransport(request_handler) - + with pytest.raises(Exception): await handler.send(request, mock_transport) - + + @pytest.mark.asyncio async def test_redirect_on_scheme_change_removes_auth_header(): """Test that if a request is redirected to a different scheme, the Authorization header is removed""" + def request_handler(request: httpx.Request): - if request.url == "http://example.com": - return httpx.Response( - 200, - ) + if request.url == "http://example.com": + return httpx.Response(200, ) return httpx.Response( TEMPORARY_REDIRECT, headers={LOCATION_HEADER: "http://example.com"}, ) + handler = RedirectHandler(RedirectHandlerOption(allow_redirect_on_scheme_change=True)) request = httpx.Request( 'GET', @@ -196,20 +201,21 @@ def request_handler(request: httpx.Request): assert resp.status_code == 200 assert resp.request != request assert AUTHORIZATION_HEADER not in resp.request.headers - + + @pytest.mark.asyncio async def test_redirect_with_same_host_keeps_auth_header(): """Test that if a request is redirected to the same host, the Authorization header is kept""" + def request_handler(request: httpx.Request): - if request.url == f"{BASE_URL}/foo": - return httpx.Response( - 200, - ) + if request.url == f"{BASE_URL}/foo": + return httpx.Response(200, ) return httpx.Response( TEMPORARY_REDIRECT, headers={LOCATION_HEADER: f"{BASE_URL}/foo"}, ) + handler = RedirectHandler(RedirectHandlerOption(allow_redirect_on_scheme_change=True)) request = httpx.Request( 'GET', @@ -221,21 +227,21 @@ def request_handler(request: httpx.Request): assert resp.status_code == 200 assert resp.request != request assert AUTHORIZATION_HEADER in resp.request.headers - - + + @pytest.mark.asyncio async def test_redirect_with_relative_url_keeps_host(): """Test that if a request is redirected to a relative url, the host is kept""" + def request_handler(request: httpx.Request): - if request.url == f"{BASE_URL}/foo": - return httpx.Response( - 200, - ) + if request.url == f"{BASE_URL}/foo": + return httpx.Response(200, ) return httpx.Response( TEMPORARY_REDIRECT, headers={LOCATION_HEADER: "/foo"}, ) + handler = RedirectHandler(RedirectHandlerOption(allow_redirect_on_scheme_change=True)) request = httpx.Request( 'GET', @@ -248,12 +254,14 @@ def request_handler(request: httpx.Request): assert resp.request != request assert AUTHORIZATION_HEADER in resp.request.headers assert resp.request.url == f"{BASE_URL}/foo" - + + @pytest.mark.asyncio async def test_max_redirects_exceeded(): """Test that if the maximum number of redirects is exceeded, an exception is raised""" + def request_handler(request: httpx.Request): - if request.url == f"{BASE_URL}/foo": + if request.url == f"{BASE_URL}/foo": return httpx.Response( TEMPORARY_REDIRECT, headers={LOCATION_HEADER: "/bar"}, @@ -262,6 +270,7 @@ def request_handler(request: httpx.Request): TEMPORARY_REDIRECT, headers={LOCATION_HEADER: "/foo"}, ) + handler = RedirectHandler(RedirectHandlerOption(allow_redirect_on_scheme_change=True)) request = httpx.Request( 'GET', @@ -271,4 +280,4 @@ def request_handler(request: httpx.Request): mock_transport = httpx.MockTransport(request_handler) with pytest.raises(Exception) as e: await handler.send(request, mock_transport) - assert "Too many redirects" in str(e.value) \ No newline at end of file + assert "Too many redirects" in str(e.value) diff --git a/packages/http/httpx/tests/middleware_tests/test_retry_handler.py b/packages/http/httpx/tests/middleware_tests/test_retry_handler.py index 884e9ed5..72f661fc 100644 --- a/packages/http/httpx/tests/middleware_tests/test_retry_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_retry_handler.py @@ -152,14 +152,15 @@ def test_get_retry_after_http_date(): retry_handler = RetryHandler() assert retry_handler._get_retry_after(response) < 120 - + + @pytest.mark.asyncio async def test_ok_response_not_retried(): """Test that a 200 response is not retried""" + def request_handler(request: httpx.Request): - return httpx.Response( - 200, - ) + return httpx.Response(200, ) + handler = RetryHandler() request = httpx.Request( 'GET', @@ -171,18 +172,17 @@ def request_handler(request: httpx.Request): assert resp.request assert resp.request == request assert RETRY_ATTEMPT not in resp.request.headers - + + @pytest.mark.asyncio async def test_retries_valid(): """Test that a valid response is retried""" + def request_handler(request: httpx.Request): if RETRY_ATTEMPT in request.headers: - return httpx.Response( - 200, - ) - return httpx.Response( - SERVICE_UNAVAILABLE, - ) + return httpx.Response(200, ) + return httpx.Response(SERVICE_UNAVAILABLE, ) + handler = RetryHandler() request = httpx.Request( 'GET', @@ -194,17 +194,16 @@ def request_handler(request: httpx.Request): assert RETRY_ATTEMPT in resp.request.headers assert resp.request.headers[RETRY_ATTEMPT] == '1' + @pytest.mark.asyncio async def test_should_retry_false(): """Test that a request is not retried if should_retry is set to False""" + def request_handler(request: httpx.Request): if RETRY_ATTEMPT in request.headers: - return httpx.Response( - 200, - ) - return httpx.Response( - TOO_MANY_REQUESTS, - ) + return httpx.Response(200, ) + return httpx.Response(TOO_MANY_REQUESTS, ) + handler = RetryHandler(RetryHandlerOption(10, 1, False)) request = httpx.Request( 'GET', @@ -215,18 +214,19 @@ def request_handler(request: httpx.Request): assert resp.status_code == TOO_MANY_REQUESTS assert RETRY_ATTEMPT not in resp.request.headers + @pytest.mark.asyncio async def test_returns_same_status_code_if_delay_greater_than_max_delay(): """Test that a request is delayed based on the Retry-After header""" + def request_handler(request: httpx.Request): if RETRY_ATTEMPT in request.headers: - return httpx.Response( - 200, - ) + return httpx.Response(200, ) return httpx.Response( TOO_MANY_REQUESTS, headers={RETRY_AFTER: "20"}, ) + handler = RetryHandler(RetryHandlerOption(10, 1, True)) request = httpx.Request( 'GET', @@ -236,38 +236,29 @@ def request_handler(request: httpx.Request): resp = await handler.send(request, mock_transport) assert resp.status_code == 429 assert RETRY_ATTEMPT not in resp.request.headers - + + @pytest.mark.asyncio async def test_retry_options_apply_per_request(): """Test that a request options are applied per request""" + def request_handler(request: httpx.Request): if "request_1" in request.headers: - return httpx.Response( - SERVICE_UNAVAILABLE, - ) - return httpx.Response( - GATEWAY_TIMEOUT, - ) + return httpx.Response(SERVICE_UNAVAILABLE, ) + return httpx.Response(GATEWAY_TIMEOUT, ) + handler = RetryHandler(RetryHandlerOption(10, 2, True)) - + # Requet 1 - request = httpx.Request( - 'GET', - BASE_URL, - headers={"request_1": "request_1_header"} - ) + request = httpx.Request('GET', BASE_URL, headers={"request_1": "request_1_header"}) mock_transport = httpx.MockTransport(request_handler) resp = await handler.send(request, mock_transport) assert resp.status_code == SERVICE_UNAVAILABLE assert 'request_1' in resp.request.headers assert resp.request.headers[RETRY_ATTEMPT] == '2' - + # Request 2 - request = httpx.Request( - 'GET', - BASE_URL, - headers={"request_2": "request_2_header"} - ) + request = httpx.Request('GET', BASE_URL, headers={"request_2": "request_2_header"}) mock_transport = httpx.MockTransport(request_handler) resp = await handler.send(request, mock_transport) assert resp.status_code == GATEWAY_TIMEOUT diff --git a/packages/http/httpx/tests/test_httpx_request_adapter.py b/packages/http/httpx/tests/test_httpx_request_adapter.py index 549662cc..0e5e0f5a 100644 --- a/packages/http/httpx/tests/test_httpx_request_adapter.py +++ b/packages/http/httpx/tests/test_httpx_request_adapter.py @@ -21,7 +21,6 @@ BASE_URL = "https://graph.microsoft.com" - def test_create_request_adapter(auth_provider): request_adapter = HttpxRequestAdapter(auth_provider) assert request_adapter._authentication_provider is auth_provider @@ -180,10 +179,10 @@ async def test_throw_failed_responses_not_apierror( with pytest.raises(Exception) as e: span = mock_otel_span await request_adapter.throw_failed_responses(resp, mock_error_500_map, span, span) - assert ("The server returned an unexpected status code and the error registered" - " for this code failed to deserialize") in str( - e.value.message - ) + assert ( + "The server returned an unexpected status code and the error registered" + " for this code failed to deserialize" + ) in str(e.value.message) @pytest.mark.asyncio @@ -200,7 +199,8 @@ async def test_throw_failed_responses_4XX( span = mock_otel_span await request_adapter.throw_failed_responses(resp, mock_apierror_map, span, span) assert str(e.value.message) == "Resource not found" - + + @pytest.mark.asyncio async def test_throw_failed_responses_5XX( request_adapter, mock_apierror_map, mock_error_object, mock_otel_span @@ -215,7 +215,8 @@ async def test_throw_failed_responses_5XX( span = mock_otel_span await request_adapter.throw_failed_responses(resp, mock_apierror_map, span, span) assert str(e.value.message) == "Custom Internal Server Error" - + + @pytest.mark.asyncio async def test_throw_failed_responses_XXX( request_adapter, mock_apierror_XXX_map, mock_error_object, mock_otel_span @@ -232,7 +233,6 @@ async def test_throw_failed_responses_XXX( assert str(e.value.message) == "OdataError" - @pytest.mark.asyncio async def test_send_async(request_adapter, request_info, mock_user_response, mock_user): request_adapter.get_http_response_message = AsyncMock(return_value=mock_user_response) @@ -392,9 +392,10 @@ async def test_retries_on_cae_failure( call( request_info_mock, { - "claims": - ("eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbH" - "VlIjoiMTYwNDEwNjY1MSJ9fX0") + "claims": ( + "eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbH" + "VlIjoiMTYwNDEwNjY1MSJ9fX0" + ) }, ), ] diff --git a/packages/http/httpx/tests/test_kiota_client_factory.py b/packages/http/httpx/tests/test_kiota_client_factory.py index ef8b564b..eec62d70 100644 --- a/packages/http/httpx/tests/test_kiota_client_factory.py +++ b/packages/http/httpx/tests/test_kiota_client_factory.py @@ -3,13 +3,8 @@ from kiota_http.kiota_client_factory import KiotaClientFactory from kiota_http.middleware import ( - AsyncKiotaTransport, - MiddlewarePipeline, - ParametersNameDecodingHandler, - RedirectHandler, - RetryHandler, - UrlReplaceHandler, - HeadersInspectionHandler + AsyncKiotaTransport, MiddlewarePipeline, ParametersNameDecodingHandler, RedirectHandler, + RetryHandler, UrlReplaceHandler, HeadersInspectionHandler ) from kiota_http.middleware.options import RedirectHandlerOption, RetryHandlerOption from kiota_http.middleware.user_agent_handler import UserAgentHandler @@ -21,7 +16,8 @@ def test_create_with_default_middleware(): assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) - + + def test_create_with_default_middleware_custom_client(): """Test creation of HTTP Client using default middleware while providing a custom client""" @@ -32,7 +28,8 @@ def test_create_with_default_middleware_custom_client(): assert isinstance(client, httpx.AsyncClient) assert client.timeout == httpx.Timeout(connect=10, read=20, write=20, pool=20) assert isinstance(client._transport, AsyncKiotaTransport) - + + def test_create_with_default_middleware_custom_client_with_proxy(): """Test creation of HTTP Client using default middleware while providing a custom client""" @@ -78,7 +75,8 @@ def test_create_with_custom_middleware(): assert isinstance(client._transport, AsyncKiotaTransport) pipeline = client._transport.pipeline assert isinstance(pipeline._first_middleware, RetryHandler) - + + def test_create_with_custom_middleware_custom_client(): """Test creation of HTTP Client using custom middleware while providing a custom client""" @@ -93,6 +91,7 @@ def test_create_with_custom_middleware_custom_client(): pipeline = client._transport.pipeline assert isinstance(pipeline._first_middleware, RetryHandler) + def test_create_with_custom_middleware_custom_client_with_proxy(): """Test creation of HTTP Client using custom middleware while providing a custom client""" @@ -115,7 +114,7 @@ def test_create_with_custom_middleware_custom_client_with_proxy(): assert isinstance(transport, AsyncKiotaTransport) pipeline = transport.pipeline assert isinstance(pipeline._first_middleware, RetryHandler) - + def test_get_default_middleware(): """Test fetching of default middleware with no custom options passed"""