Skip to content

sync sources for https://github.com/microsoft/kiota-http-python/pull/434 #351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/http/httpx/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 18 additions & 12 deletions packages/http/httpx/kiota_http/httpx_request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down
6 changes: 4 additions & 2 deletions packages/http/httpx/kiota_http/middleware/redirect_handler.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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:
Expand Down
8 changes: 5 additions & 3 deletions packages/http/httpx/kiota_http/middleware/retry_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from kiota_abstractions.request_option import RequestOption
from opentelemetry.semconv.trace import SpanAttributes

from httpx import AsyncBaseTransport, Request, Response

Expand Down
16 changes: 11 additions & 5 deletions packages/http/httpx/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -26,6 +28,7 @@ def auth_provider():
def request_info():
return RequestInformation()


@pytest.fixture
def mock_async_transport():
return MockTransport()
Expand Down Expand Up @@ -57,27 +60,32 @@ def mock_error_500_map():
"500": Exception("Internal Server Error"),
}


@pytest.fixture
def mock_apierror_map(sample_headers):
return {
"400": APIError("Resource not found", 400, 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)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion packages/http/httpx/tests/helpers/mock_async_transport.py
Original file line number Diff line number Diff line change
@@ -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"})
return httpx.Response(
200,
request=request,
content=b'Hello World',
headers={
"Content-Type": "application/json",
"test": "test_response_header"
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'}


Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
Loading