From b1fea22ee1237c704b1ff96a1b6f27855702f6da Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 11:04:12 +0300 Subject: [PATCH 01/60] Add opentelemetry libraries Signed-off-by: Musale Martin --- requirements-dev.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index c01c96a..e528ae7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -94,3 +94,6 @@ sniffio==1.3.0 uritemplate==4.1.1 +opentelemetry-api==1.19.0 + +opentelemetry-sdk==1.19.0 \ No newline at end of file From e0819028abad4ee4f54c71c2ad43b5ced149a746 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 11:16:21 +0300 Subject: [PATCH 02/60] Remove unrecognized options from disabled prop Signed-off-by: Musale Martin --- .pylintrc | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.pylintrc b/.pylintrc index 1b5b658..5ead169 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,11 +60,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=long-suffix, - old-ne-operator, - old-octal-literal, - non-ascii-bytes-literal, - raw-checker-failed, +disable=raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, @@ -72,7 +68,6 @@ disable=long-suffix, useless-suppression, deprecated-pragma, use-symbolic-message-instead, - eq-without-hash, too-few-public-methods, missing-module-docstring, missing-class-docstring, From a0db9c4cc00c0632bb1800d4ac48edf9e6293931 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 12:30:09 +0300 Subject: [PATCH 03/60] Add custom exceptions for kiota http Signed-off-by: Musale Martin --- kiota_http/exceptions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 kiota_http/exceptions.py diff --git a/kiota_http/exceptions.py b/kiota_http/exceptions.py new file mode 100644 index 0000000..fdfca91 --- /dev/null +++ b/kiota_http/exceptions.py @@ -0,0 +1,12 @@ +"""Exceptions raised in Kiota HTTP.""" + + +class KiotaHTTPException(Exception): + """Base class for Kiota HTTP exceptions.""" + + +class BackingstoreException(KiotaHTTPException): + """Raised for the backing store.""" + +class DeserializationException(KiotaHTTPException): + """Raised for deserialization.""" \ No newline at end of file From 409927866c86370f89c631eaaa547aca0d1e0b02 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 12:30:27 +0300 Subject: [PATCH 04/60] Add o11y options Signed-off-by: Musale Martin --- kiota_http/observability_options.py | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 kiota_http/observability_options.py diff --git a/kiota_http/observability_options.py b/kiota_http/observability_options.py new file mode 100644 index 0000000..2b06fcf --- /dev/null +++ b/kiota_http/observability_options.py @@ -0,0 +1,49 @@ +"""Observability options for python http package.""" +from kiota_abstractions.request_option import RequestOption + + +class ObservabilityOptions(RequestOption): + """Defines the metrics, tracing and logging configurations.""" + OBSERVABILITY_OPTION_KEY = "ObservabilityOptionKey" + + def __init__(self, enabled: bool = True, include_euii_attributes: bool=True)-> None: + """Initialize the observability options. + + Args: + enabled(bool): whether to enable the ObservabilityOptions in the middleware chain. + include_euii_attributes(bool): whether to include attributes that + could contain EUII information likr URLS. + """ + self._enabled = enabled + self._include_euii_attributes = include_euii_attributes + + @property + def enabled(self)-> bool: + """Gets the enabled option value.""" + return self._enabled + + @enabled.setter + def enabled(self, value: bool): + """sets whether to enable ObservabilityOptions in the middleware chain.""" + self._enabled = value + + @property + def include_euii_attributes(self)-> bool: + """Returns whether to include EUII attributes.""" + return self._include_euii_attributes + + @include_euii_attributes.setter + def include_euii_attributes(self, value: bool)-> None: + """Sets whether to include EUII attributes.""" + self._include_euii_attributes = value + + @staticmethod + def get_key()-> str: + """The middleware key name.""" + return ObservabilityOptions.OBSERVABILITY_OPTION_KEY + + @staticmethod + def getTracerInstrumentationName()-> str: + """Returns the instrumentation name used for tracing""" + return "com.microsoft.com:microsoft-kiota-http-httpx" + \ No newline at end of file From e567d808bed925b81223570bdf1c8fbe6b31647b Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:06:37 +0300 Subject: [PATCH 05/60] Fix exceptions formatting Signed-off-by: Musale Martin --- kiota_http/exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kiota_http/exceptions.py b/kiota_http/exceptions.py index fdfca91..1c1b80f 100644 --- a/kiota_http/exceptions.py +++ b/kiota_http/exceptions.py @@ -8,5 +8,6 @@ class KiotaHTTPException(Exception): class BackingstoreException(KiotaHTTPException): """Raised for the backing store.""" + class DeserializationException(KiotaHTTPException): - """Raised for deserialization.""" \ No newline at end of file + """Raised for deserialization.""" From 8431167da9e95f0199bd0142d6da788391eb6ce8 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:07:08 +0300 Subject: [PATCH 06/60] Fix o11y options formatting Signed-off-by: Musale Martin --- kiota_http/observability_options.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/kiota_http/observability_options.py b/kiota_http/observability_options.py index 2b06fcf..5595130 100644 --- a/kiota_http/observability_options.py +++ b/kiota_http/observability_options.py @@ -6,7 +6,7 @@ class ObservabilityOptions(RequestOption): """Defines the metrics, tracing and logging configurations.""" OBSERVABILITY_OPTION_KEY = "ObservabilityOptionKey" - def __init__(self, enabled: bool = True, include_euii_attributes: bool=True)-> None: + def __init__(self, enabled: bool = True, include_euii_attributes: bool = True) -> None: """Initialize the observability options. Args: @@ -18,7 +18,7 @@ def __init__(self, enabled: bool = True, include_euii_attributes: bool=True)-> N self._include_euii_attributes = include_euii_attributes @property - def enabled(self)-> bool: + def enabled(self) -> bool: """Gets the enabled option value.""" return self._enabled @@ -28,22 +28,21 @@ def enabled(self, value: bool): self._enabled = value @property - def include_euii_attributes(self)-> bool: + def include_euii_attributes(self) -> bool: """Returns whether to include EUII attributes.""" return self._include_euii_attributes @include_euii_attributes.setter - def include_euii_attributes(self, value: bool)-> None: + def include_euii_attributes(self, value: bool) -> None: """Sets whether to include EUII attributes.""" self._include_euii_attributes = value @staticmethod - def get_key()-> str: + def get_key() -> str: """The middleware key name.""" return ObservabilityOptions.OBSERVABILITY_OPTION_KEY @staticmethod - def getTracerInstrumentationName()-> str: + def get_tracer_instrumentation_name() -> str: """Returns the instrumentation name used for tracing""" return "com.microsoft.com:microsoft-kiota-http-httpx" - \ No newline at end of file From 1ce332d7211715cb78d01eafe5e3b0fd4be47cb1 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:07:56 +0300 Subject: [PATCH 07/60] Add decoding uri string method and fix formatting Signed-off-by: Musale Martin --- .../middleware/parameters_name_decoding_handler.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kiota_http/middleware/parameters_name_decoding_handler.py b/kiota_http/middleware/parameters_name_decoding_handler.py index 3c8fe5a..181b29b 100644 --- a/kiota_http/middleware/parameters_name_decoding_handler.py +++ b/kiota_http/middleware/parameters_name_decoding_handler.py @@ -25,7 +25,7 @@ def __init__( async def send( self, request: httpx.Request, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: #type: ignore + ) -> httpx.Response: # type: ignore """To execute the current middleware Args: @@ -37,7 +37,7 @@ async def send( """ current_options = self._get_current_options(request) - updated_url: str = str(request.url) #type: ignore + updated_url: str = str(request.url) # type: ignore if all( [ current_options, current_options.enabled, '%' in updated_url, @@ -59,7 +59,13 @@ def _get_current_options(self, request: httpx.Request) -> ParametersNameDecoding Returns: ParametersNameDecodingHandlerOption: The options to used. """ - current_options =request.options.get( # type:ignore + current_options = request.options.get( # type:ignore ParametersNameDecodingHandlerOption.get_key(), self.options ) return current_options + + def decode_uri_encoded_string(self, original: str) -> str: + """Decodes a uri encoded string .""" + if '%' in original: + return unquote(original) + return original From 27397519d5fd2c4c88fb0c71d87f1a72f1d07a27 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:14:50 +0300 Subject: [PATCH 08/60] Add telemetry for send_async method Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 322 +++++++++++++++++++++------- 1 file changed, 243 insertions(+), 79 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 36a9c91..3d23e53 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -1,5 +1,8 @@ +"""HTTPX client request adapter.""" + from datetime import datetime from typing import Any, Dict, Generic, List, Optional, TypeVar, Union +from urllib import parse import httpx from kiota_abstractions.api_client_builder import ( @@ -20,12 +23,25 @@ SerializationWriterFactoryRegistry, ) from kiota_abstractions.store import BackingStoreFactory, BackingStoreFactorySingleton +from opentelemetry import trace +from opentelemetry.semconv.trace import SpanAttributes + +from kiota_http.exceptions import BackingstoreException, DeserializationException +from kiota_http.middleware.parameters_name_decoding_handler import ParametersNameDecodingHandler +from ._version import VERSION from .kiota_client_factory import KiotaClientFactory from .middleware.options import ResponseHandlerOption +from .observability_options import ObservabilityOptions ResponseType = Union[str, int, float, bool, datetime, bytes] ModelType = TypeVar("ModelType", bound=Parsable) +RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" +ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" +ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" +DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" + +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) class HttpxRequestAdapter(RequestAdapter, Generic[ModelType]): @@ -36,9 +52,9 @@ def __init__( parse_node_factory: ParseNodeFactory = ParseNodeFactoryRegistry(), serialization_writer_factory: SerializationWriterFactory = SerializationWriterFactoryRegistry(), - http_client: httpx.AsyncClient = KiotaClientFactory.create_with_default_middleware() + http_client: httpx.AsyncClient = KiotaClientFactory.create_with_default_middleware(), + observability_options=ObservabilityOptions(), ) -> None: - if not authentication_provider: raise TypeError("Authentication provider cannot be null") self._authentication_provider = authentication_provider @@ -50,10 +66,12 @@ def __init__( self._serialization_writer_factory = serialization_writer_factory if not http_client: raise TypeError("Http Client cannot be null") + if not observability_options: + observability_options = ObservabilityOptions() self._http_client = http_client - - self._base_url: str = '' + self._base_url: str = "" + self.observability_options = observability_options @property def base_url(self) -> str: @@ -86,20 +104,44 @@ def get_response_content_type(self, response: httpx.Response) -> Optional[str]: header = response.headers.get("content-type") if not header: return None - segments = header.lower().split(';') + segments = header.lower().split(";") if not segments: return None return segments[0] + def start_tracing_span(self, request_info: RequestInformation, method: str) -> trace.Span: + """Creates an Opentelemetry tracer and starts the parent span. + + Args: + request_info(RequestInformation): the request object. + method(str): name of the invoker. + + Returns: + The parent span. + """ + name_handler = ParametersNameDecodingHandler() + uri_template = name_handler.decode_uri_encoded_string(request_info.url_template) + parent_span_name = f"{method} - {uri_template}" + span = tracer.start_span(parent_span_name) + return span + + def _start_local_tracing_span(self, name: str, parent_span: trace.Span) -> trace.Span: + """Helper function to start a span locally with the parent context.""" + _context = trace.set_span_in_context(parent_span) + span = tracer.start_span(name, context=_context) + return span + async def send_async( - self, request_info: RequestInformation, parsable_factory: ParsableFactory, - error_map: Dict[str, ParsableFactory] + self, + request_info: RequestInformation, + parsable_factory: ParsableFactory, + error_map: Dict[str, ParsableFactory], ) -> Optional[ModelType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. Args: request_info (RequestInformation): the request info to execute. - parsable_factory (ParsableFactory): the class of the response model + parsable_factory (ParsableFactory): the class of the response model to deserialize the response into. error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. @@ -107,25 +149,37 @@ async def send_async( Returns: ModelType: the deserialized response model. """ - if not request_info: - raise TypeError("Request info cannot be null") - - response = await self.get_http_response_message(request_info) - - response_handler = self.get_response_handler(request_info) - if response_handler: - return await response_handler.handle_response_async(response, error_map) - - await self.throw_failed_responses(response, error_map) - if self._should_return_none(response): - return None - root_node = await self.get_root_parse_node(response) - result = root_node.get_object_value(parsable_factory) - return result + parent_span = self.start_tracing_span(request_info, "send_async") + try: + if not request_info: + exc = TypeError("Request info cannot be null") + parent_span.record_exception(exc) + raise exc + + response = await self.get_http_response_message(request_info, parent_span) + + response_handler = self.get_response_handler(request_info) + if response_handler: + parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY) + return await response_handler.handle_response_async(response, error_map) + + await self.throw_failed_responses(response, error_map, parent_span, parent_span) + if self._should_return_none(response): + return None + root_node = await self.get_root_parse_node(response, parent_span, parent_span) + _deserialized_span = self._start_local_tracing_span("get_object_value", parent_span) + value = root_node.get_object_value(parsable_factory) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + _deserialized_span.end() + return value + finally: + parent_span.end() async def send_collection_async( - self, request_info: RequestInformation, parsable_factory: ParsableFactory, - error_map: Dict[str, ParsableFactory] + self, + request_info: RequestInformation, + parsable_factory: ParsableFactory, + error_map: Dict[str, ParsableFactory], ) -> Optional[List[ModelType]]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. @@ -150,13 +204,20 @@ async def send_collection_async( await self.throw_failed_responses(response, error_map) if self._should_return_none(response): return None - root_node = await self.get_root_parse_node(response) - result = root_node.get_collection_of_object_values(parsable_factory) + with tracer.start_as_current_span("get_collection_of_object_values") as span: + try: + root_node = await self.get_root_parse_node(response) + result = root_node.get_collection_of_object_values(parsable_factory) + except DeserializationException as exc: + span.record_exception(exc) + span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc))) return result async def send_collection_of_primitive_async( - self, request_info: RequestInformation, response_type: ResponseType, - error_map: Dict[str, ParsableFactory] + self, + request_info: RequestInformation, + response_type: ResponseType, + error_map: Dict[str, ParsableFactory], ) -> Optional[List[ResponseType]]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. @@ -186,8 +247,10 @@ async def send_collection_of_primitive_async( return root_node.get_collection_of_primitive_values(response_type) async def send_primitive_async( - self, request_info: RequestInformation, response_type: ResponseType, - error_map: Dict[str, ParsableFactory] + self, + request_info: RequestInformation, + response_type: ResponseType, + error_map: Dict[str, ParsableFactory], ) -> Optional[ResponseType]: """Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. @@ -257,74 +320,142 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto self._parse_node_factory = enable_backing_store_for_parse_node_factory( self._parse_node_factory ) - self._serialization_writer_factory = enable_backing_store_for_serialization_writer_factory( - self._serialization_writer_factory + self._serialization_writer_factory = ( + enable_backing_store_for_serialization_writer_factory( + self._serialization_writer_factory + ) ) if not any([self._serialization_writer_factory, self._parse_node_factory]): - raise Exception("Unable to enable backing store") + raise BackingstoreException("Unable to enable backing store") if backing_store_factory: BackingStoreFactorySingleton.__instance = backing_store_factory - async def get_root_parse_node(self, response: httpx.Response) -> ParseNode: - payload = response.content - response_content_type = self.get_response_content_type(response) - if not response_content_type: - raise Exception("No response content type found for deserialization") - - return self._parse_node_factory.get_root_parse_node(response_content_type, payload) + async def get_root_parse_node( + self, + response: httpx.Response, + parent_span: trace.Span, + attribute_span: trace.Span, + ) -> ParseNode: + span = self._start_local_tracing_span("get_root_parse_node", parent_span) + + try: + payload = response.content + response_content_type = self.get_response_content_type(response) + if not response_content_type: + raise DeserializationException("No response content type found for deserialization") + return self._parse_node_factory.get_root_parse_node(response_content_type, payload) + finally: + span.end() def _should_return_none(self, response: httpx.Response) -> bool: return response.status_code == 204 async def throw_failed_responses( - self, response: httpx.Response, error_map: Dict[str, ParsableFactory] + self, + response: httpx.Response, + error_map: Dict[str, ParsableFactory], + parent_span: trace.Span, + attribute_span: trace.Span, ) -> None: if response.is_success: return + try: + # TODO: set status for the parent only? + attribute_span.set_status(trace.StatusCode.ERROR) - response_status_code = response.status_code - response_status_code_str = str(response_status_code) - response_headers = response.headers - - if not error_map: - raise APIError( - "The server returned an unexpected status code and no error class is registered" - f" for this code {response_status_code}", response_status_code, response_headers + _throw_failed_resp_span = self._start_local_tracing_span( + "throw_failed_responses", parent_span ) - if (response_status_code_str not in error_map) and ( - (400 <= response_status_code < 500 and '4XX' not in error_map) or - (500 <= response_status_code < 600 and '5XX' not in error_map) - ): - raise APIError( - "The server returned an unexpected status code and no error class is registered" - f" for this code {response_status_code}", response_status_code, response_headers - ) - - error_class = None - if response_status_code_str in error_map: - error_class = error_map[response_status_code_str] - elif 400 <= response_status_code < 500: - error_class = error_map['4XX'] - elif 500 <= response_status_code < 600: - error_class = error_map['5XX'] - root_node = await self.get_root_parse_node(response) - error = root_node.get_object_value(error_class) - - if isinstance(error, APIError): - error.response_headers = response_headers - error.response_status_code = response_status_code - raise error - raise APIError( - f"Unexpected error type: {type(error)}", response_status_code, response_headers + response_status_code = response.status_code + response_status_code_str = str(response_status_code) + response_headers = response.headers + + _throw_failed_resp_span.set_attribute("status", response_status_code) + _throw_failed_resp_span.set_attribute(ERROR_MAPPING_FOUND_KEY, bool(error_map)) + if not error_map: + exc = APIError( + "The server returned an unexpected status code and no error class is registered" + f" for this code {response_status_code}", + response_status_code, + response_headers, + ) + # set this or ignore as description in set_status? + _throw_failed_resp_span.set_attribute("status_message", "received_error_response") + # TODO: set status for just this span or the parent as well? + _throw_failed_resp_span.set_status(trace.StatusCode.ERROR, str(exc)) + attribute_span.record_exception(exc) + raise exc + + if (response_status_code_str not in error_map) and ( + (400 <= response_status_code < 500 and "4XX" not in error_map) or + (500 <= response_status_code < 600 and "5XX" not in error_map) + ): + exc = APIError( + "The server returned an unexpected status code and no error class is registered" + f" for this code {response_status_code}", + response_status_code, + response_headers, + ) + attribute_span.record_exception(exc) + raise exc + _throw_failed_resp_span.set_attribute("status_message", "received_error_response") + + error_class = None + if response_status_code_str in error_map: + error_class = error_map[response_status_code_str] + elif 400 <= response_status_code < 500: + error_class = error_map["4XX"] + elif 500 <= response_status_code < 600: + error_class = error_map["5XX"] + + root_node = await self.get_root_parse_node( + response, _throw_failed_resp_span, _throw_failed_resp_span + ) + attribute_span.set_attribute(ERROR_BODY_FOUND_KEY, bool(root_node)) + + _get_obj_ctx = trace.set_span_in_context(_throw_failed_resp_span) + _get_obj_span = tracer.start_span("get_object_value", context=_get_obj_ctx) + + error = root_node.get_object_value(error_class) + if isinstance(error, APIError): + error.response_headers = response_headers + error.response_status_code = response_status_code + exc = error + else: + exc = APIError( + f"Unexpected error type: {type(error)}", + response_status_code, + response_headers, + ) + _get_obj_span.end() + raise exc + finally: + _throw_failed_resp_span.end() + + async def get_http_response_message( + self, request_info: RequestInformation, parent_span: trace.Span + ) -> httpx.Response: + _get_http_resp_span = self._start_local_tracing_span( + "get_http_response_message", parent_span ) - - async def get_http_response_message(self, request_info: RequestInformation) -> httpx.Response: self.set_base_url_for_request_information(request_info) await self._authentication_provider.authenticate_request(request_info) - request = self.get_request_from_request_information(request_info) + request = self.get_request_from_request_information( + request_info, _get_http_resp_span, parent_span + ) resp = await self._http_client.send(request) + parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code) + if http_version := resp.http_version: + parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, http_version) + + if content_length := resp.headers.get("Content-Length", None): + parent_span.set_attribute(SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, content_length) + + if content_type := resp.headers.get("Content-Type", None): + parent_span.set_attribute("http.response_content_type", content_type) + _get_http_resp_span.end() return resp def get_response_handler(self, request_info: RequestInformation) -> Any: @@ -337,15 +468,48 @@ def set_base_url_for_request_information(self, request_info: RequestInformation) request_info.path_parameters["baseurl"] = self.base_url def get_request_from_request_information( - self, request_info: RequestInformation + self, + request_info: RequestInformation, + parent_span: trace.Span, + attribute_span: trace.Span, ) -> httpx.Request: + _get_request_span = self._start_local_tracing_span( + "get_request_from_request_information", parent_span + ) + url = parse.urlparse(request_info.url) + otel_attributes = { + SpanAttributes.HTTP_METHOD: request_info.http_method, + "http.port": url.port, + SpanAttributes.HTTP_HOST: url.hostname, + SpanAttributes.HTTP_SCHEME: url.scheme, + "http.uri_template": request_info.url_template, + } + + if self.observability_options.include_euii_attributes: + otel_attributes.update({"http.uri": url.geturl()}) + request = self._http_client.build_request( method=request_info.http_method.value, url=request_info.url, headers=request_info.request_headers, content=request_info.content, ) + request_options = { + self.observability_options.get_key(): self.observability_options, + "parent_span": parent_span, + } request.options = request_info.request_options # type:ignore + request.options.update(**request_options) # type:ignore + + if content_length := request.headers.get("Content-Length", None): + otel_attributes.update({SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) + + if content_type := request.headers.get("Content-Type", None): + otel_attributes.update({"http.request_content_type": content_type}) + attribute_span.set_attributes(otel_attributes) + _get_request_span.set_attributes(otel_attributes) + _get_request_span.end() + return request async def convert_to_native_async(self, request_info: RequestInformation) -> httpx.Request: From 1e7d67027c9780df441be5ecf96f1cd303564bd9 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:41:53 +0300 Subject: [PATCH 09/60] Add send_collection_async tracing Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 77 +++++++++++++++++++---------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 3d23e53..299fbc3 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -41,7 +41,8 @@ ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) +tracer = trace.get_tracer( + ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) class HttpxRequestAdapter(RequestAdapter, Generic[ModelType]): @@ -120,7 +121,8 @@ def start_tracing_span(self, request_info: RequestInformation, method: str) -> t The parent span. """ name_handler = ParametersNameDecodingHandler() - uri_template = name_handler.decode_uri_encoded_string(request_info.url_template) + uri_template = name_handler.decode_uri_encoded_string( + request_info.url_template) parent_span_name = f"{method} - {uri_template}" span = tracer.start_span(parent_span_name) return span @@ -167,9 +169,11 @@ async def send_async( if self._should_return_none(response): return None root_node = await self.get_root_parse_node(response, parent_span, parent_span) - _deserialized_span = self._start_local_tracing_span("get_object_value", parent_span) + _deserialized_span = self._start_local_tracing_span( + "get_object_value", parent_span) value = root_node.get_object_value(parsable_factory) - parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + parent_span.set_attribute( + DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) _deserialized_span.end() return value finally: @@ -193,24 +197,30 @@ async def send_collection_async( Returns: ModelType: the deserialized response model collection. """ + parent_span = self.start_tracing_span( + request_info, "send_collection_async") if not request_info: - raise TypeError("Request info cannot be null") - response = await self.get_http_response_message(request_info) + exc = TypeError("Request info cannot be null") + parent_span.record_exception(exc) + raise exc + response = await self.get_http_response_message(request_info, parent_span) response_handler = self.get_response_handler(request_info) if response_handler: + parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY) return await response_handler.handle_response_async(response, error_map) - await self.throw_failed_responses(response, error_map) + await self.throw_failed_responses(response, error_map, parent_span, parent_span) if self._should_return_none(response): return None - with tracer.start_as_current_span("get_collection_of_object_values") as span: - try: - root_node = await self.get_root_parse_node(response) - result = root_node.get_collection_of_object_values(parsable_factory) - except DeserializationException as exc: - span.record_exception(exc) - span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc))) + + _deserialized_span = self._start_local_tracing_span( + "get_collection_of_object_values", parent_span) + root_node = await self.get_root_parse_node(response, parent_span, parent_span) + result = root_node.get_collection_of_object_values(parsable_factory) + parent_span.set_attribute( + DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) + _deserialized_span.end() return result async def send_collection_of_primitive_async( @@ -336,13 +346,15 @@ async def get_root_parse_node( parent_span: trace.Span, attribute_span: trace.Span, ) -> ParseNode: - span = self._start_local_tracing_span("get_root_parse_node", parent_span) + span = self._start_local_tracing_span( + "get_root_parse_node", parent_span) try: payload = response.content response_content_type = self.get_response_content_type(response) if not response_content_type: - raise DeserializationException("No response content type found for deserialization") + raise DeserializationException( + "No response content type found for deserialization") return self._parse_node_factory.get_root_parse_node(response_content_type, payload) finally: span.end() @@ -371,8 +383,10 @@ async def throw_failed_responses( response_status_code_str = str(response_status_code) response_headers = response.headers - _throw_failed_resp_span.set_attribute("status", response_status_code) - _throw_failed_resp_span.set_attribute(ERROR_MAPPING_FOUND_KEY, bool(error_map)) + _throw_failed_resp_span.set_attribute( + "status", response_status_code) + _throw_failed_resp_span.set_attribute( + ERROR_MAPPING_FOUND_KEY, bool(error_map)) if not error_map: exc = APIError( "The server returned an unexpected status code and no error class is registered" @@ -381,9 +395,11 @@ async def throw_failed_responses( response_headers, ) # set this or ignore as description in set_status? - _throw_failed_resp_span.set_attribute("status_message", "received_error_response") + _throw_failed_resp_span.set_attribute( + "status_message", "received_error_response") # TODO: set status for just this span or the parent as well? - _throw_failed_resp_span.set_status(trace.StatusCode.ERROR, str(exc)) + _throw_failed_resp_span.set_status( + trace.StatusCode.ERROR, str(exc)) attribute_span.record_exception(exc) raise exc @@ -399,7 +415,8 @@ async def throw_failed_responses( ) attribute_span.record_exception(exc) raise exc - _throw_failed_resp_span.set_attribute("status_message", "received_error_response") + _throw_failed_resp_span.set_attribute( + "status_message", "received_error_response") error_class = None if response_status_code_str in error_map: @@ -415,7 +432,8 @@ async def throw_failed_responses( attribute_span.set_attribute(ERROR_BODY_FOUND_KEY, bool(root_node)) _get_obj_ctx = trace.set_span_in_context(_throw_failed_resp_span) - _get_obj_span = tracer.start_span("get_object_value", context=_get_obj_ctx) + _get_obj_span = tracer.start_span( + "get_object_value", context=_get_obj_ctx) error = root_node.get_object_value(error_class) if isinstance(error, APIError): @@ -446,20 +464,24 @@ async def get_http_response_message( request_info, _get_http_resp_span, parent_span ) resp = await self._http_client.send(request) - parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code) + parent_span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, resp.status_code) if http_version := resp.http_version: parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, 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( + SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 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_content_type", content_type) _get_http_resp_span.end() return resp def get_response_handler(self, request_info: RequestInformation) -> Any: - response_handler_option = request_info.request_options.get(ResponseHandlerOption.get_key()) + response_handler_option = request_info.request_options.get( + ResponseHandlerOption.get_key()) if response_handler_option: return response_handler_option.response_handler return None @@ -502,7 +524,8 @@ def get_request_from_request_information( request.options.update(**request_options) # type:ignore if content_length := request.headers.get("Content-Length", None): - otel_attributes.update({SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) + otel_attributes.update( + {SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) if content_type := request.headers.get("Content-Type", None): otel_attributes.update({"http.request_content_type": content_type}) From f0252b2c67cadec3549428087de72f507de14711 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:42:30 +0300 Subject: [PATCH 10/60] Fix formatting errors Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 60 ++++++++++------------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 299fbc3..ed5497a 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -41,8 +41,7 @@ ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" -tracer = trace.get_tracer( - ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) class HttpxRequestAdapter(RequestAdapter, Generic[ModelType]): @@ -121,8 +120,7 @@ def start_tracing_span(self, request_info: RequestInformation, method: str) -> t The parent span. """ name_handler = ParametersNameDecodingHandler() - uri_template = name_handler.decode_uri_encoded_string( - request_info.url_template) + uri_template = name_handler.decode_uri_encoded_string(request_info.url_template) parent_span_name = f"{method} - {uri_template}" span = tracer.start_span(parent_span_name) return span @@ -169,11 +167,9 @@ async def send_async( if self._should_return_none(response): return None root_node = await self.get_root_parse_node(response, parent_span, parent_span) - _deserialized_span = self._start_local_tracing_span( - "get_object_value", parent_span) + _deserialized_span = self._start_local_tracing_span("get_object_value", parent_span) value = root_node.get_object_value(parsable_factory) - parent_span.set_attribute( - DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) _deserialized_span.end() return value finally: @@ -197,8 +193,7 @@ async def send_collection_async( Returns: ModelType: the deserialized response model collection. """ - parent_span = self.start_tracing_span( - request_info, "send_collection_async") + parent_span = self.start_tracing_span(request_info, "send_collection_async") if not request_info: exc = TypeError("Request info cannot be null") parent_span.record_exception(exc) @@ -215,11 +210,11 @@ async def send_collection_async( return None _deserialized_span = self._start_local_tracing_span( - "get_collection_of_object_values", parent_span) + "get_collection_of_object_values", parent_span + ) root_node = await self.get_root_parse_node(response, parent_span, parent_span) result = root_node.get_collection_of_object_values(parsable_factory) - parent_span.set_attribute( - DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) _deserialized_span.end() return result @@ -346,15 +341,13 @@ async def get_root_parse_node( parent_span: trace.Span, attribute_span: trace.Span, ) -> ParseNode: - span = self._start_local_tracing_span( - "get_root_parse_node", parent_span) + span = self._start_local_tracing_span("get_root_parse_node", parent_span) try: payload = response.content response_content_type = self.get_response_content_type(response) if not response_content_type: - raise DeserializationException( - "No response content type found for deserialization") + raise DeserializationException("No response content type found for deserialization") return self._parse_node_factory.get_root_parse_node(response_content_type, payload) finally: span.end() @@ -383,10 +376,8 @@ async def throw_failed_responses( response_status_code_str = str(response_status_code) response_headers = response.headers - _throw_failed_resp_span.set_attribute( - "status", response_status_code) - _throw_failed_resp_span.set_attribute( - ERROR_MAPPING_FOUND_KEY, bool(error_map)) + _throw_failed_resp_span.set_attribute("status", response_status_code) + _throw_failed_resp_span.set_attribute(ERROR_MAPPING_FOUND_KEY, bool(error_map)) if not error_map: exc = APIError( "The server returned an unexpected status code and no error class is registered" @@ -395,11 +386,9 @@ async def throw_failed_responses( response_headers, ) # set this or ignore as description in set_status? - _throw_failed_resp_span.set_attribute( - "status_message", "received_error_response") + _throw_failed_resp_span.set_attribute("status_message", "received_error_response") # TODO: set status for just this span or the parent as well? - _throw_failed_resp_span.set_status( - trace.StatusCode.ERROR, str(exc)) + _throw_failed_resp_span.set_status(trace.StatusCode.ERROR, str(exc)) attribute_span.record_exception(exc) raise exc @@ -415,8 +404,7 @@ async def throw_failed_responses( ) attribute_span.record_exception(exc) raise exc - _throw_failed_resp_span.set_attribute( - "status_message", "received_error_response") + _throw_failed_resp_span.set_attribute("status_message", "received_error_response") error_class = None if response_status_code_str in error_map: @@ -432,8 +420,7 @@ async def throw_failed_responses( attribute_span.set_attribute(ERROR_BODY_FOUND_KEY, bool(root_node)) _get_obj_ctx = trace.set_span_in_context(_throw_failed_resp_span) - _get_obj_span = tracer.start_span( - "get_object_value", context=_get_obj_ctx) + _get_obj_span = tracer.start_span("get_object_value", context=_get_obj_ctx) error = root_node.get_object_value(error_class) if isinstance(error, APIError): @@ -464,24 +451,20 @@ async def get_http_response_message( request_info, _get_http_resp_span, parent_span ) resp = await self._http_client.send(request) - parent_span.set_attribute( - SpanAttributes.HTTP_STATUS_CODE, resp.status_code) + parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code) if http_version := resp.http_version: parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, 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(SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 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_content_type", content_type) _get_http_resp_span.end() return resp def get_response_handler(self, request_info: RequestInformation) -> Any: - response_handler_option = request_info.request_options.get( - ResponseHandlerOption.get_key()) + response_handler_option = request_info.request_options.get(ResponseHandlerOption.get_key()) if response_handler_option: return response_handler_option.response_handler return None @@ -524,8 +507,7 @@ def get_request_from_request_information( request.options.update(**request_options) # type:ignore if content_length := request.headers.get("Content-Length", None): - otel_attributes.update( - {SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) + otel_attributes.update({SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) if content_type := request.headers.get("Content-Type", None): otel_attributes.update({"http.request_content_type": content_type}) From 766bc16ab622f08a6cd5c8afb020b316aec8f042 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:50:51 +0300 Subject: [PATCH 11/60] Add tracing on send_collection_of_primitive_async Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 37 ++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index ed5497a..3ca37e3 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -236,20 +236,35 @@ async def send_collection_of_primitive_async( Returns: Optional[List[ModelType]]: he deserialized response model collection. """ - if not request_info: - raise TypeError("Request info cannot be null") + parent_span = self.start_tracing_span(request_info, "send_collection_of_primitive_async") + try: + if not request_info: + exc = TypeError("Request info cannot be null") + parent_span.record_exception(exc) + raise exc - response = await self.get_http_response_message(request_info) + response = await self.get_http_response_message(request_info, parent_span) - response_handler = self.get_response_handler(request_info) - if response_handler: - return await response_handler.handle_response_async(response, error_map) + response_handler = self.get_response_handler(request_info) + if response_handler: + parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY) + return await response_handler.handle_response_async(response, error_map) - await self.throw_failed_responses(response, error_map) - if self._should_return_none(response): - return None - root_node = await self.get_root_parse_node(response) - return root_node.get_collection_of_primitive_values(response_type) + await self.throw_failed_responses(response, error_map, parent_span, parent_span) + if self._should_return_none(response): + return None + root_node = await self.get_root_parse_node(response, parent_span, parent_span) + + _deserialized_span = self._start_local_tracing_span( + "get_collection_of_primitive_values", parent_span + ) + root_node = await self.get_root_parse_node(response, parent_span, parent_span) + values = root_node.get_collection_of_primitive_values(response_type) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, values.__class__.__name__) + _deserialized_span.end() + return values + finally: + parent_span.end() async def send_primitive_async( self, From ca5cd5c09f16ac57a4fa173cf80cd038381f5105 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 20:04:12 +0300 Subject: [PATCH 12/60] Add tracing on send_primitive_async Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 64 ++++++++++++++++++----------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 3ca37e3..f2ae7d3 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -284,32 +284,50 @@ async def send_primitive_async( Returns: ResponseType: the deserialized primitive response model. """ - if not request_info: - raise TypeError("Request info cannot be null") + parent_span = self.start_tracing_span(request_info, "send_primitive_async") + try: + if not request_info: + exc = TypeError("Request info cannot be null") + parent_span.record_exception(exc) + raise exc - response = await self.get_http_response_message(request_info) + response = await self.get_http_response_message(request_info, parent_span) - response_handler = self.get_response_handler(request_info) - if response_handler: - return await response_handler.handle_response_async(response, error_map) + response_handler = self.get_response_handler(request_info) + if response_handler: + parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY) + return await response_handler.handle_response_async(response, error_map) - await self.throw_failed_responses(response, error_map) - if self._should_return_none(response): - return None - if response_type == "bytes": - return response.content - root_node = await self.get_root_parse_node(response) - if response_type == "str": - return root_node.get_str_value() - if response_type == "int": - return root_node.get_int_value() - if response_type == "float": - return root_node.get_float_value() - if response_type == "bool": - return root_node.get_bool_value() - if response_type == "datetime": - return root_node.get_datetime_value() - raise TypeError(f"Unable to deserialize type: {response_type!r}") + await self.throw_failed_responses(response, error_map, parent_span, parent_span) + if self._should_return_none(response): + return None + if response_type == "bytes": + return response.content + _deserialized_span = self._start_local_tracing_span("get_root_parse_node", parent_span) + root_node = await self.get_root_parse_node(response, parent_span, parent_span) + value = None + if response_type == "str": + value = root_node.get_str_value() + if response_type == "int": + value = root_node.get_int_value() + if response_type == "float": + value = root_node.get_float_value() + if response_type == "bool": + value = root_node.get_bool_value() + if response_type == "datetime": + value = root_node.get_datetime_value() + if value: + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + _deserialized_span.end() + return value + + exc = TypeError(f"Unable to deserialize type: {response_type!r}") + parent_span.record_exception(exc) + _deserialized_span.end() + raise exc + + finally: + parent_span.end() async def send_no_response_content_async( self, request_info: RequestInformation, error_map: Dict[str, ParsableFactory] From c0314d72805da153b0fdb492202beaafedbbb57f Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 20:06:47 +0300 Subject: [PATCH 13/60] Add tracing on send_no_response_content_async Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index f2ae7d3..dd29dbe 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -339,16 +339,23 @@ async def send_no_response_content_async( error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. """ - if not request_info: - raise TypeError("Request info cannot be null") + parent_span = self.start_tracing_span(request_info, "send_no_response_content_async") + try: + if not request_info: + exc = TypeError("Request info cannot be null") + parent_span.record_exception(exc) + raise exc - response = await self.get_http_response_message(request_info) + response = await self.get_http_response_message(request_info, parent_span) - response_handler = self.get_response_handler(request_info) - if response_handler: - return await response_handler.handle_response_async(response, error_map) + response_handler = self.get_response_handler(request_info) + if response_handler: + parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY) + return await response_handler.handle_response_async(response, error_map) - await self.throw_failed_responses(response, error_map) + await self.throw_failed_responses(response, error_map, parent_span, parent_span) + finally: + parent_span.end() def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFactory]) -> None: """Enables the backing store proxies for the SerializationWriters and ParseNodes in use. From 9385f8a9d08038c007a936c3c963bb400b416dbd Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 20:11:00 +0300 Subject: [PATCH 14/60] Add tracing on convert_to_native_async Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index dd29dbe..dbf74de 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -558,10 +558,18 @@ def get_request_from_request_information( return request async def convert_to_native_async(self, request_info: RequestInformation) -> httpx.Request: - if request_info is None: - raise ValueError("request information must be provided") + parent_span = self.start_tracing_span(request_info, "convert_to_native_async") + try: + if request_info is None: + exc = ValueError("request information must be provided") + parent_span.record_exception(exc) + raise exc - await self._authentication_provider.authenticate_request(request_info) + await self._authentication_provider.authenticate_request(request_info) - request = self.get_request_from_request_information(request_info) - return request + request = self.get_request_from_request_information( + request_info, parent_span, parent_span + ) + return request + finally: + parent_span.end() From 5d89a57b98cc901de61e036df01002ad77542cd0 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 20:12:35 +0300 Subject: [PATCH 15/60] Fix closing of parent span in send_collection_async Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 43 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index dbf74de..699afda 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -194,29 +194,32 @@ async def send_collection_async( ModelType: the deserialized response model collection. """ parent_span = self.start_tracing_span(request_info, "send_collection_async") - if not request_info: - exc = TypeError("Request info cannot be null") - parent_span.record_exception(exc) - raise exc - response = await self.get_http_response_message(request_info, parent_span) + try: + if not request_info: + exc = TypeError("Request info cannot be null") + parent_span.record_exception(exc) + raise exc + response = await self.get_http_response_message(request_info, parent_span) - response_handler = self.get_response_handler(request_info) - if response_handler: - parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY) - return await response_handler.handle_response_async(response, error_map) + response_handler = self.get_response_handler(request_info) + if response_handler: + parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY) + return await response_handler.handle_response_async(response, error_map) - await self.throw_failed_responses(response, error_map, parent_span, parent_span) - if self._should_return_none(response): - return None + await self.throw_failed_responses(response, error_map, parent_span, parent_span) + if self._should_return_none(response): + return None - _deserialized_span = self._start_local_tracing_span( - "get_collection_of_object_values", parent_span - ) - root_node = await self.get_root_parse_node(response, parent_span, parent_span) - result = root_node.get_collection_of_object_values(parsable_factory) - parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) - _deserialized_span.end() - return result + _deserialized_span = self._start_local_tracing_span( + "get_collection_of_object_values", parent_span + ) + root_node = await self.get_root_parse_node(response, parent_span, parent_span) + result = root_node.get_collection_of_object_values(parsable_factory) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) + _deserialized_span.end() + return result + finally: + parent_span.end() async def send_collection_of_primitive_async( self, From 4f74a138b2d1607bf4bd2e3f5c296c32822a0191 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 20:33:25 +0300 Subject: [PATCH 16/60] Fix failing tests with a mock span Signed-off-by: Musale Martin --- .../parameters_name_decoding_handler.py | 2 +- tests/conftest.py | 15 +++++++- .../test_url_replace_handler.py | 21 ++++++----- tests/test_httpx_request_adapter.py | 35 ++++++++++++------- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/kiota_http/middleware/parameters_name_decoding_handler.py b/kiota_http/middleware/parameters_name_decoding_handler.py index 181b29b..98de58d 100644 --- a/kiota_http/middleware/parameters_name_decoding_handler.py +++ b/kiota_http/middleware/parameters_name_decoding_handler.py @@ -66,6 +66,6 @@ def _get_current_options(self, request: httpx.Request) -> ParametersNameDecoding def decode_uri_encoded_string(self, original: str) -> str: """Decodes a uri encoded string .""" - if '%' in original: + if original and '%' in original: return unquote(original) return original diff --git a/tests/conftest.py b/tests/conftest.py index 8dcb26f..33043e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from kiota_abstractions.api_error import APIError from kiota_abstractions.authentication import AnonymousAuthenticationProvider from kiota_abstractions.request_information import RequestInformation +from opentelemetry import trace from kiota_http.httpx_request_adapter import HttpxRequestAdapter @@ -167,15 +168,27 @@ def mock_primitive(mocker): def mock_primitive_response(mocker): return httpx.Response(200, json=22.3, headers={"Content-Type": "application/json"}) + @pytest.fixture def mock_primitive_response_bytes(mocker): return httpx.Response( 200, content=b'Hello World', - headers={"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"} + headers={ + "Content-Type": + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + } ) @pytest.fixture def mock_no_content_response(mocker): return httpx.Response(204, json="Radom JSON", headers={"Content-Type": "application/json"}) + + +tracer = trace.get_tracer(__name__) + + +@pytest.fixture +def mock_otel_span(): + return tracer.start_span("mock") diff --git a/tests/middleware_tests/test_url_replace_handler.py b/tests/middleware_tests/test_url_replace_handler.py index c94b942..8b63511 100644 --- a/tests/middleware_tests/test_url_replace_handler.py +++ b/tests/middleware_tests/test_url_replace_handler.py @@ -7,6 +7,7 @@ ORIGINAL_URL = "https://graph.microsoft.com/users/user-id-to-replace/messages" REPLACED_URL = "https://graph.microsoft.com/me/messages" + def test_no_config(): """ Test that default values are used if no custom confguration is passed @@ -22,24 +23,28 @@ def test_custom_options_with_handler_disabled(): """ Test that default configuration is overrriden if custom configuration is provided """ - handler = UrlReplaceHandler(options=UrlReplaceHandlerOption( - enabled=False, - replacement_pairs={"/users/user-id-to-replace": "/me"})) + handler = UrlReplaceHandler( + options=UrlReplaceHandlerOption( + enabled=False, replacement_pairs={"/users/user-id-to-replace": "/me"} + ) + ) assert not handler.options.is_enabled assert handler.options.replacement_pairs assert handler.replace_url_segment(ORIGINAL_URL, handler.options) == ORIGINAL_URL - + + def test_replace_url_segment(): """ Test that url segments corresponding to replacement pairs are replaced. """ - handler = UrlReplaceHandler(options=UrlReplaceHandlerOption( - enabled=True, - replacement_pairs={"/users/user-id-to-replace": "/me"})) + handler = UrlReplaceHandler( + options=UrlReplaceHandlerOption( + enabled=True, replacement_pairs={"/users/user-id-to-replace": "/me"} + ) + ) assert handler.options.is_enabled assert handler.options.replacement_pairs assert handler.options.get_key() == "UrlReplaceHandlerOption" assert handler.replace_url_segment(ORIGINAL_URL, handler.options) == REPLACED_URL - diff --git a/tests/test_httpx_request_adapter.py b/tests/test_httpx_request_adapter.py index f0d6617..9b6c37f 100644 --- a/tests/test_httpx_request_adapter.py +++ b/tests/test_httpx_request_adapter.py @@ -57,11 +57,12 @@ def test_set_base_url_for_request_information(request_adapter, request_info): assert request_info.path_parameters["baseurl"] == BASE_URL -def test_get_request_from_request_information(request_adapter, request_info): +def test_get_request_from_request_information(request_adapter, request_info, mock_otel_span): request_info.http_method = Method.GET request_info.url = BASE_URL request_info.content = bytes('hello world', 'utf_8') - req = request_adapter.get_request_from_request_information(request_info) + span = mock_otel_span + req = request_adapter.get_request_from_request_information(request_info, span, span) assert isinstance(req, httpx.Request) @@ -105,22 +106,25 @@ async def test_does_not_throw_failed_responses_on_success(request_adapter, simpl @pytest.mark.asyncio -async def test_throw_failed_responses_null_error_map(request_adapter, simple_error_response): +async def test_throw_failed_responses_null_error_map( + request_adapter, simple_error_response, mock_otel_span +): assert simple_error_response.text == '{"error": "not found"}' assert simple_error_response.status_code == 404 content_type = request_adapter.get_response_content_type(simple_error_response) assert content_type == 'application/json' with pytest.raises(APIError) as e: - await request_adapter.throw_failed_responses(simple_error_response, None) + span = mock_otel_span + await request_adapter.throw_failed_responses(simple_error_response, None, span, span) assert str(e.value.message) == "The server returned an unexpected status code and"\ - " no error class is registered for this code 404" + " no error class is registered for this code 404" assert e.value.response_status_code == 404 @pytest.mark.asyncio async def test_throw_failed_responses_no_error_class( - request_adapter, simple_error_response, mock_error_map + request_adapter, simple_error_response, mock_error_map, mock_otel_span ): assert simple_error_response.text == '{"error": "not found"}' assert simple_error_response.status_code == 404 @@ -128,15 +132,18 @@ async def test_throw_failed_responses_no_error_class( assert content_type == 'application/json' with pytest.raises(APIError) as e: - await request_adapter.throw_failed_responses(simple_error_response, mock_error_map) + span = mock_otel_span + await request_adapter.throw_failed_responses( + simple_error_response, mock_error_map, span, span + ) assert str(e.value.message) == "The server returned an unexpected status code and"\ - " no error class is registered for this code 404" + " no error class is registered for this code 404" assert e.value.response_status_code == 404 @pytest.mark.asyncio async def test_throw_failed_responses_not_apierror( - request_adapter, mock_error_map, mock_error_object + request_adapter, mock_error_map, mock_error_object, mock_otel_span ): request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object) resp = httpx.Response(status_code=500, headers={"Content-Type": "application/json"}) @@ -145,12 +152,15 @@ async def test_throw_failed_responses_not_apierror( assert content_type == 'application/json' with pytest.raises(Exception) as e: - await request_adapter.throw_failed_responses(resp, mock_error_map) + span = mock_otel_span + await request_adapter.throw_failed_responses(resp, mock_error_map, span, span) assert str(e.value.message) == "Unexpected error type: " @pytest.mark.asyncio -async def test_throw_failed_responses(request_adapter, mock_apierror_map, mock_error_object): +async def test_throw_failed_responses( + request_adapter, mock_apierror_map, mock_error_object, mock_otel_span +): request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object) resp = httpx.Response(status_code=500, headers={"Content-Type": "application/json"}) assert resp.status_code == 500 @@ -158,7 +168,8 @@ async def test_throw_failed_responses(request_adapter, mock_apierror_map, mock_e assert content_type == 'application/json' with pytest.raises(APIError) as e: - await request_adapter.throw_failed_responses(resp, mock_apierror_map) + 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" From 41da962a8b60a8faefb25191e8106ab0cf240c6f Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 20:43:08 +0300 Subject: [PATCH 17/60] Fix code smells and rename exceptions Signed-off-by: Musale Martin --- kiota_http/_exceptions.py | 17 +++++++++++++++ kiota_http/exceptions.py | 13 ------------ kiota_http/httpx_request_adapter.py | 33 +++++++++++++---------------- 3 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 kiota_http/_exceptions.py delete mode 100644 kiota_http/exceptions.py diff --git a/kiota_http/_exceptions.py b/kiota_http/_exceptions.py new file mode 100644 index 0000000..62d5956 --- /dev/null +++ b/kiota_http/_exceptions.py @@ -0,0 +1,17 @@ +"""Exceptions raised in Kiota HTTP.""" + + +class KiotaHTTPXError(Exception): + """Base class for Kiota HTTP exceptions.""" + + +class BackingstoreError(KiotaHTTPXError): + """Raised for the backing store.""" + + +class DeserializationError(KiotaHTTPXError): + """Raised for deserialization.""" + + +class RequestError(KiotaHTTPXError): + """Raised for request building errors.""" diff --git a/kiota_http/exceptions.py b/kiota_http/exceptions.py deleted file mode 100644 index 1c1b80f..0000000 --- a/kiota_http/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Exceptions raised in Kiota HTTP.""" - - -class KiotaHTTPException(Exception): - """Base class for Kiota HTTP exceptions.""" - - -class BackingstoreException(KiotaHTTPException): - """Raised for the backing store.""" - - -class DeserializationException(KiotaHTTPException): - """Raised for deserialization.""" diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 699afda..0b292d1 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -26,7 +26,7 @@ from opentelemetry import trace from opentelemetry.semconv.trace import SpanAttributes -from kiota_http.exceptions import BackingstoreException, DeserializationException +from kiota_http._exceptions import BackingstoreError, DeserializationError, RequestError from kiota_http.middleware.parameters_name_decoding_handler import ParametersNameDecodingHandler from ._version import VERSION @@ -36,10 +36,12 @@ ResponseType = Union[str, int, float, bool, datetime, bytes] ModelType = TypeVar("ModelType", bound=Parsable) + RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" +REQUEST_IS_NULL = RequestError("Request info cannot be null") tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) @@ -152,9 +154,8 @@ async def send_async( parent_span = self.start_tracing_span(request_info, "send_async") try: if not request_info: - exc = TypeError("Request info cannot be null") - parent_span.record_exception(exc) - raise exc + parent_span.record_exception(REQUEST_IS_NULL) + raise REQUEST_IS_NULL response = await self.get_http_response_message(request_info, parent_span) @@ -196,9 +197,8 @@ async def send_collection_async( parent_span = self.start_tracing_span(request_info, "send_collection_async") try: if not request_info: - exc = TypeError("Request info cannot be null") - parent_span.record_exception(exc) - raise exc + parent_span.record_exception(REQUEST_IS_NULL) + raise REQUEST_IS_NULL response = await self.get_http_response_message(request_info, parent_span) response_handler = self.get_response_handler(request_info) @@ -242,9 +242,8 @@ async def send_collection_of_primitive_async( parent_span = self.start_tracing_span(request_info, "send_collection_of_primitive_async") try: if not request_info: - exc = TypeError("Request info cannot be null") - parent_span.record_exception(exc) - raise exc + parent_span.record_exception(REQUEST_IS_NULL) + raise REQUEST_IS_NULL response = await self.get_http_response_message(request_info, parent_span) @@ -290,9 +289,8 @@ async def send_primitive_async( parent_span = self.start_tracing_span(request_info, "send_primitive_async") try: if not request_info: - exc = TypeError("Request info cannot be null") - parent_span.record_exception(exc) - raise exc + parent_span.record_exception(REQUEST_IS_NULL) + raise REQUEST_IS_NULL response = await self.get_http_response_message(request_info, parent_span) @@ -345,9 +343,8 @@ async def send_no_response_content_async( parent_span = self.start_tracing_span(request_info, "send_no_response_content_async") try: if not request_info: - exc = TypeError("Request info cannot be null") - parent_span.record_exception(exc) - raise exc + parent_span.record_exception(REQUEST_IS_NULL) + raise REQUEST_IS_NULL response = await self.get_http_response_message(request_info, parent_span) @@ -374,7 +371,7 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto ) ) if not any([self._serialization_writer_factory, self._parse_node_factory]): - raise BackingstoreException("Unable to enable backing store") + raise BackingstoreError("Unable to enable backing store") if backing_store_factory: BackingStoreFactorySingleton.__instance = backing_store_factory @@ -390,7 +387,7 @@ async def get_root_parse_node( payload = response.content response_content_type = self.get_response_content_type(response) if not response_content_type: - raise DeserializationException("No response content type found for deserialization") + raise DeserializationError("No response content type found for deserialization") return self._parse_node_factory.get_root_parse_node(response_content_type, payload) finally: span.end() From c28bbefe7352e1b57d6b408bdfc793dafdba6260 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 12:09:54 +0300 Subject: [PATCH 18/60] Add tracing to parameters_name_decoding_handler Signed-off-by: Martin Musale --- kiota_http/httpx_request_adapter.py | 4 ++-- .../parameters_name_decoding_handler.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 0b292d1..a6d6703 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -542,9 +542,9 @@ def get_request_from_request_information( request_options = { self.observability_options.get_key(): self.observability_options, "parent_span": parent_span, + **request_info.request_options } - request.options = request_info.request_options # type:ignore - request.options.update(**request_options) # type:ignore + setattr(request, 'options', request_options) if content_length := request.headers.get("Content-Length", None): otel_attributes.update({SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) diff --git a/kiota_http/middleware/parameters_name_decoding_handler.py b/kiota_http/middleware/parameters_name_decoding_handler.py index 98de58d..c13cc16 100644 --- a/kiota_http/middleware/parameters_name_decoding_handler.py +++ b/kiota_http/middleware/parameters_name_decoding_handler.py @@ -2,10 +2,16 @@ import httpx from kiota_abstractions.request_option import RequestOption +from opentelemetry import trace +from .._version import VERSION +from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import ParametersNameDecodingHandlerOption +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) +PARAMETERS_NAME_DECODING_KEY = "com.microsoft.kiota.handler.parameters_name_decoding.enable" + class ParametersNameDecodingHandler(BaseMiddleware): @@ -36,6 +42,12 @@ async def send( Response: The response object. """ current_options = self._get_current_options(request) + if options := getattr(request, "options", None): + if parent_span := options.get("parent_span", None): + _context = trace.set_span_in_context(parent_span) + span = tracer.start_span("ParametersNameDecodingHandler_send", context=_context) + span.set_attribute(PARAMETERS_NAME_DECODING_KEY, current_options.enabled) + span.end() updated_url: str = str(request.url) # type: ignore if all( @@ -59,9 +71,10 @@ def _get_current_options(self, request: httpx.Request) -> ParametersNameDecoding Returns: ParametersNameDecodingHandlerOption: The options to used. """ - current_options = request.options.get( # type:ignore - ParametersNameDecodingHandlerOption.get_key(), self.options - ) + if options := getattr(request, 'options', None): + current_options = options.get( # type:ignore + ParametersNameDecodingHandlerOption.get_key(), self.options + ) return current_options def decode_uri_encoded_string(self, original: str) -> str: From 39e0c4472702e0dd956f8fa2bcfffc721b83f873 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 12:47:48 +0300 Subject: [PATCH 19/60] Add tracing to redirect_handler Signed-off-by: Martin Musale --- kiota_http/_exceptions.py | 4 ++ kiota_http/middleware/redirect_handler.py | 54 ++++++++++++++++------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/kiota_http/_exceptions.py b/kiota_http/_exceptions.py index 62d5956..42f5816 100644 --- a/kiota_http/_exceptions.py +++ b/kiota_http/_exceptions.py @@ -15,3 +15,7 @@ class DeserializationError(KiotaHTTPXError): class RequestError(KiotaHTTPXError): """Raised for request building errors.""" + + +class RedirectError(KiotaHTTPXError): + """Raised when a redirect has errors.""" diff --git a/kiota_http/middleware/redirect_handler.py b/kiota_http/middleware/redirect_handler.py index 87966c4..e0c5835 100644 --- a/kiota_http/middleware/redirect_handler.py +++ b/kiota_http/middleware/redirect_handler.py @@ -2,10 +2,18 @@ import httpx from kiota_abstractions.request_option import RequestOption +from opentelemetry import trace +from opentelemetry.semconv.trace import SpanAttributes +from .._exceptions import RedirectError +from .._version import VERSION +from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import RedirectHandlerOption +RETRY_ENABLE_KEY = "com.microsoft.kiota.handler.retry.enable" +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) + class RedirectHandler(BaseMiddleware): """Middlware that allows us to define the redirect policy for all requests @@ -59,23 +67,35 @@ async def send( """Sends the http request object to the next middleware or redirects the request if necessary. """ - current_options = self._get_current_options(request) - - retryable = True - while retryable: - response = await super().send(request, transport) - redirect_location = self.get_redirect_location(response) - if redirect_location and current_options.should_redirect: - current_options.max_redirect -= 1 - retryable = self.increment(response, current_options.max_redirect) - new_request = self._build_redirect_request(request, response) - request = new_request - continue - - response.history = self.history - return response - - raise Exception(f"Too many redirects. {response.history}") + if options := getattr(request, "options", None): + if parent_span := options.get("parent_span", None): + _context = trace.set_span_in_context(parent_span) + _enable_span = tracer.start_span("redirect_handler_send", _context) + current_options = self._get_current_options(request) + _enable_span.set_attribute(RETRY_ENABLE_KEY, True) + _enable_span.end() + + retryable = True + _retry_span = tracer.start_span( + f"redirect_handler_send - attempt {len(self.history)}", _context + ) + while retryable: + _retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, len(self.history)) + response = await super().send(request, transport) + _retry_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + redirect_location = self.get_redirect_location(response) + if redirect_location and current_options.should_redirect: + current_options.max_redirect -= 1 + retryable = self.increment(response, current_options.max_redirect) + new_request = self._build_redirect_request(request, response) + request = new_request + continue + response.history = self.history + _retry_span.end() + exc = RedirectError(f"Too many redirects. {response.history}") + parent_span.record_exception(exc) + raise exc + return response def _get_current_options(self, request: httpx.Request) -> RedirectHandlerOption: """Returns the options to use for the request.Overries default options if From 5001484904221d477c1dabb18b758351b0ca6b3c Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 12:59:03 +0300 Subject: [PATCH 20/60] Fix redirect_handler keys and attributes used Signed-off-by: Martin Musale --- kiota_http/middleware/redirect_handler.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/kiota_http/middleware/redirect_handler.py b/kiota_http/middleware/redirect_handler.py index e0c5835..5f3cc2a 100644 --- a/kiota_http/middleware/redirect_handler.py +++ b/kiota_http/middleware/redirect_handler.py @@ -11,7 +11,8 @@ from .middleware import BaseMiddleware from .options import RedirectHandlerOption -RETRY_ENABLE_KEY = "com.microsoft.kiota.handler.retry.enable" +REDIRECT_ENABLE_KEY = "com.microsoft.kiota.handler.redirect.enable" +REDIRECT_COUNT_KEY = "com.microsoft.kiota.handler.redirect.count" tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) @@ -72,26 +73,26 @@ async def send( _context = trace.set_span_in_context(parent_span) _enable_span = tracer.start_span("redirect_handler_send", _context) current_options = self._get_current_options(request) - _enable_span.set_attribute(RETRY_ENABLE_KEY, True) + _enable_span.set_attribute(REDIRECT_ENABLE_KEY, True) _enable_span.end() retryable = True - _retry_span = tracer.start_span( - f"redirect_handler_send - attempt {len(self.history)}", _context + _redirect_span = tracer.start_span( + f"redirect_handler_send - redirect {len(self.history)}", _context ) while retryable: - _retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, len(self.history)) response = await super().send(request, transport) - _retry_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + _redirect_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) redirect_location = self.get_redirect_location(response) if redirect_location and current_options.should_redirect: current_options.max_redirect -= 1 retryable = self.increment(response, current_options.max_redirect) + _redirect_span.set_attribute(REDIRECT_COUNT_KEY, len(self.history)) new_request = self._build_redirect_request(request, response) request = new_request continue response.history = self.history - _retry_span.end() + _redirect_span.end() exc = RedirectError(f"Too many redirects. {response.history}") parent_span.record_exception(exc) raise exc From d6dbc27c8b6077c3f36b850c1bead77016d0a0a5 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 13:09:41 +0300 Subject: [PATCH 21/60] Fix formatting issues Signed-off-by: Martin Musale --- kiota_http/middleware/redirect_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kiota_http/middleware/redirect_handler.py b/kiota_http/middleware/redirect_handler.py index 5f3cc2a..1b12d6f 100644 --- a/kiota_http/middleware/redirect_handler.py +++ b/kiota_http/middleware/redirect_handler.py @@ -82,7 +82,9 @@ async def send( ) while retryable: response = await super().send(request, transport) - _redirect_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + _redirect_span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, response.status_code + ) redirect_location = self.get_redirect_location(response) if redirect_location and current_options.should_redirect: current_options.max_redirect -= 1 From 8c397a9863da92c758e23d4551361c146490dcc2 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 13:10:06 +0300 Subject: [PATCH 22/60] Add tracing to retry_handler Signed-off-by: Martin Musale --- kiota_http/middleware/retry_handler.py | 75 ++++++++++++++++---------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/kiota_http/middleware/retry_handler.py b/kiota_http/middleware/retry_handler.py index 35ae6e3..dafb46b 100644 --- a/kiota_http/middleware/retry_handler.py +++ b/kiota_http/middleware/retry_handler.py @@ -6,10 +6,16 @@ import httpx from kiota_abstractions.request_option import RequestOption +from opentelemetry import trace +from opentelemetry.semconv.trace import SpanAttributes +from .._version import VERSION +from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import RetryHandlerOption +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) + class RetryHandler(BaseMiddleware): """ @@ -66,36 +72,49 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport """ Sends the http request object to the next middleware or retries the request if necessary. """ - current_options = self._get_current_options(request) response = None retry_count = 0 - retry_valid = current_options.should_retry - - while retry_valid: - start_time = time.time() - if retry_count > 0: - request.headers.update({'retry-attempt': f'{retry_count}'}) - response = await super().send(request, transport) - # Check if the request needs to be retried based on the response method - # and status code - if self.should_retry(request, current_options, response): - # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_count, current_options) - - # Get the delay time between retries - delay = self.get_delay_time(retry_count, response) - - if retry_valid and delay < current_options.max_delay: - time.sleep(delay) - end_time = time.time() - current_options.max_delay -= (end_time - start_time) - # increment the count for retries - retry_count += 1 - - continue - break - if response is None: - response = await super().send(request, transport) + + if options := getattr(request, "options", None): + if parent_span := options.get("parent_span", None): + _context = trace.set_span_in_context(parent_span) + _enable_span = tracer.start_span("retry_handler_send", _context) + current_options = self._get_current_options(request) + _enable_span.set_attribute("com.microsoft.kiota.handler.retry.enable", True) + _enable_span.end() + retry_valid = current_options.should_retry + _retry_span = tracer.start_span( + f"retry_handler_send - attempt {retry_count}", _context + ) + while retry_valid: + start_time = time.time() + if retry_count > 0: + request.headers.update({'retry-attempt': f'{retry_count}'}) + response = await super().send(request, transport) + # Check if the request needs to be retried based on the response method + # and status code + if self.should_retry(request, current_options, response): + # check that max retries has not been hit + retry_valid = self.check_retry_valid(retry_count, current_options) + + # Get the delay time between retries + delay = self.get_delay_time(retry_count, response) + + if retry_valid and delay < current_options.max_delay: + time.sleep(delay) + end_time = time.time() + current_options.max_delay -= (end_time - start_time) + # increment the count for retries + retry_count += 1 + _retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, retry_count) + continue + break + if response is None: + response = await super().send(request, transport) + _retry_span.set_attributes( + SpanAttributes.HTTP_STATUS_CODE, response.status_code + ) + _retry_span.end() return response def _get_current_options(self, request: httpx.Request) -> RetryHandlerOption: From 9e85b72b35296188a86a94051704d5578b99d1fc Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 13:21:34 +0300 Subject: [PATCH 23/60] Add tracing to url_replace_handler Signed-off-by: Martin Musale --- kiota_http/middleware/url_replace_handler.py | 24 ++++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/kiota_http/middleware/url_replace_handler.py b/kiota_http/middleware/url_replace_handler.py index aaf8117..1085f79 100644 --- a/kiota_http/middleware/url_replace_handler.py +++ b/kiota_http/middleware/url_replace_handler.py @@ -1,9 +1,15 @@ import httpx from kiota_abstractions.request_option import RequestOption +from opentelemetry import trace +from opentelemetry.semconv.trace import SpanAttributes +from .._version import VERSION +from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import UrlReplaceHandlerOption +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) + class UrlReplaceHandler(BaseMiddleware): @@ -30,12 +36,20 @@ async def send( Returns: Response: The response object. """ - current_options = self._get_current_options(request) + response: httpx.Response + if options := getattr(request, "options", None): + if parent_span := options.get("parent_span", None): + _context = trace.set_span_in_context(parent_span) + _enable_span = tracer.start_span("url_replace_handler_send", _context) + _enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True) + current_options = self._get_current_options(request) - url_string: str = str(request.url) #type: ignore - url_string = self.replace_url_segment(url_string, current_options) - request.url = httpx.URL(url_string) - response = await super().send(request, transport) + url_string: str = str(request.url) #type: ignore + 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)) + response = await super().send(request, transport) + _enable_span.end() return response def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOption: From 547993fcd4834d5afc1c0d2f75c5368c67c3b5ab Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 13:27:55 +0300 Subject: [PATCH 24/60] Add tracing to user_agent_handler Signed-off-by: Martin Musale --- kiota_http/middleware/user_agent_handler.py | 30 ++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/kiota_http/middleware/user_agent_handler.py b/kiota_http/middleware/user_agent_handler.py index 243d591..6353825 100644 --- a/kiota_http/middleware/user_agent_handler.py +++ b/kiota_http/middleware/user_agent_handler.py @@ -1,9 +1,15 @@ from httpx import AsyncBaseTransport, Request, Response from kiota_abstractions.request_option import RequestOption +from opentelemetry import trace +from opentelemetry.semconv.trace import SpanAttributes +from .._version import VERSION +from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import UserAgentHandlerOption +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) + class UserAgentHandler(BaseMiddleware): """ @@ -19,14 +25,20 @@ async def send(self, request: Request, transport: AsyncBaseTransport) -> Respons Checks if the request has a User-Agent header and updates it if the platform config allows. """ - if self.options and self.options.is_enabled: - value = f"{self.options.product_name}/{self.options.product_version}" - - user_agent = request.headers.get("User-Agent", "") - if not user_agent: - request.headers.update({"User-Agent": value}) - else: - if value not in user_agent: - request.headers.update({"User-Agent": f"{user_agent} {value}"}) + if options := getattr(request, "options", None): + if parent_span := options.get("parent_span", None): + _context = trace.set_span_in_context(parent_span) + _span = tracer.start_span("redirect_handler_send", _context) + if self.options and self.options.is_enabled: + _span.set_attribute("com.microsoft.kiota.handler.useragent.enable", True) + value = f"{self.options.product_name}/{self.options.product_version}" + + user_agent = request.headers.get("User-Agent", "") + if not user_agent: + request.headers.update({"User-Agent": value}) + else: + if value not in user_agent: + request.headers.update({"User-Agent": f"{user_agent} {value}"}) + _span.end() return await super().send(request, transport) From 1d123a345e583ce41f96042c74c754ca37fcd7b5 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 13:30:10 +0300 Subject: [PATCH 25/60] Fix method names used in span creation Signed-off-by: Martin Musale --- kiota_http/middleware/redirect_handler.py | 4 ++-- kiota_http/middleware/retry_handler.py | 4 ++-- kiota_http/middleware/url_replace_handler.py | 2 +- kiota_http/middleware/user_agent_handler.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kiota_http/middleware/redirect_handler.py b/kiota_http/middleware/redirect_handler.py index 1b12d6f..58fdea2 100644 --- a/kiota_http/middleware/redirect_handler.py +++ b/kiota_http/middleware/redirect_handler.py @@ -71,14 +71,14 @@ async def send( if options := getattr(request, "options", None): if parent_span := options.get("parent_span", None): _context = trace.set_span_in_context(parent_span) - _enable_span = tracer.start_span("redirect_handler_send", _context) + _enable_span = tracer.start_span("RedirectHandler_send", _context) current_options = self._get_current_options(request) _enable_span.set_attribute(REDIRECT_ENABLE_KEY, True) _enable_span.end() retryable = True _redirect_span = tracer.start_span( - f"redirect_handler_send - redirect {len(self.history)}", _context + f"RedirectHandler_send - redirect {len(self.history)}", _context ) while retryable: response = await super().send(request, transport) diff --git a/kiota_http/middleware/retry_handler.py b/kiota_http/middleware/retry_handler.py index dafb46b..9c72125 100644 --- a/kiota_http/middleware/retry_handler.py +++ b/kiota_http/middleware/retry_handler.py @@ -78,13 +78,13 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport if options := getattr(request, "options", None): if parent_span := options.get("parent_span", None): _context = trace.set_span_in_context(parent_span) - _enable_span = tracer.start_span("retry_handler_send", _context) + _enable_span = tracer.start_span("RetryHandler_send", _context) current_options = self._get_current_options(request) _enable_span.set_attribute("com.microsoft.kiota.handler.retry.enable", True) _enable_span.end() retry_valid = current_options.should_retry _retry_span = tracer.start_span( - f"retry_handler_send - attempt {retry_count}", _context + f"RetryHandler_send - attempt {retry_count}", _context ) while retry_valid: start_time = time.time() diff --git a/kiota_http/middleware/url_replace_handler.py b/kiota_http/middleware/url_replace_handler.py index 1085f79..373f13a 100644 --- a/kiota_http/middleware/url_replace_handler.py +++ b/kiota_http/middleware/url_replace_handler.py @@ -40,7 +40,7 @@ async def send( if options := getattr(request, "options", None): if parent_span := options.get("parent_span", None): _context = trace.set_span_in_context(parent_span) - _enable_span = tracer.start_span("url_replace_handler_send", _context) + _enable_span = tracer.start_span("UrlReplaceHandler_send", _context) _enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True) current_options = self._get_current_options(request) diff --git a/kiota_http/middleware/user_agent_handler.py b/kiota_http/middleware/user_agent_handler.py index 6353825..548e103 100644 --- a/kiota_http/middleware/user_agent_handler.py +++ b/kiota_http/middleware/user_agent_handler.py @@ -28,7 +28,7 @@ async def send(self, request: Request, transport: AsyncBaseTransport) -> Respons if options := getattr(request, "options", None): if parent_span := options.get("parent_span", None): _context = trace.set_span_in_context(parent_span) - _span = tracer.start_span("redirect_handler_send", _context) + _span = tracer.start_span("UserAgentHandler_send", _context) if self.options and self.options.is_enabled: _span.set_attribute("com.microsoft.kiota.handler.useragent.enable", True) value = f"{self.options.product_name}/{self.options.product_version}" From 472f9eb8739c4a0104cd55ad7162b1128dcfe864 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 14:08:34 +0300 Subject: [PATCH 26/60] Refactor retry handler to reduce complexity Signed-off-by: Martin Musale --- kiota_http/middleware/retry_handler.py | 80 ++++++++++++++------------ 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/kiota_http/middleware/retry_handler.py b/kiota_http/middleware/retry_handler.py index 9c72125..aa6986d 100644 --- a/kiota_http/middleware/retry_handler.py +++ b/kiota_http/middleware/retry_handler.py @@ -75,47 +75,51 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport response = None retry_count = 0 + _span = self._create_observability_span(request, "RetryHandler_send") + if _span is not None: + current_options = self._get_current_options(request) + _span.set_attribute("com.microsoft.kiota.handler.retry.enable", True) + _span.end() + retry_valid = current_options.should_retry + _retry_span = self._create_observability_span( + request, f"RetryHandler_send - attempt {retry_count}" + ) + while retry_valid: + start_time = time.time() + response = await super().send(request, transport) + _retry_span.set_attributes(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + # check that max retries has not been hit + retry_valid = self.check_retry_valid(retry_count, current_options) + + # Get the delay time between retries + delay = self.get_delay_time(retry_count, response) + + # Check if the request needs to be retried based on the response method + # and status code + should_retry = self.should_retry(request, current_options, response) + if all(should_retry, retry_valid, delay < current_options.max_delay): + time.sleep(delay) + end_time = time.time() + current_options.max_delay -= (end_time - start_time) + # 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) + continue + break + if response is None: + response = await super().send(request, transport) + _retry_span.end() + return response + + def _create_observability_span(request: httpx.Request, span_name: str) -> Optional[trace.Span]: + """Gets the parent_span from the request options and creates a new span.""" + _span: trace.Span = None if options := getattr(request, "options", None): if parent_span := options.get("parent_span", None): _context = trace.set_span_in_context(parent_span) - _enable_span = tracer.start_span("RetryHandler_send", _context) - current_options = self._get_current_options(request) - _enable_span.set_attribute("com.microsoft.kiota.handler.retry.enable", True) - _enable_span.end() - retry_valid = current_options.should_retry - _retry_span = tracer.start_span( - f"RetryHandler_send - attempt {retry_count}", _context - ) - while retry_valid: - start_time = time.time() - if retry_count > 0: - request.headers.update({'retry-attempt': f'{retry_count}'}) - response = await super().send(request, transport) - # Check if the request needs to be retried based on the response method - # and status code - if self.should_retry(request, current_options, response): - # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_count, current_options) - - # Get the delay time between retries - delay = self.get_delay_time(retry_count, response) - - if retry_valid and delay < current_options.max_delay: - time.sleep(delay) - end_time = time.time() - current_options.max_delay -= (end_time - start_time) - # increment the count for retries - retry_count += 1 - _retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, retry_count) - continue - break - if response is None: - response = await super().send(request, transport) - _retry_span.set_attributes( - SpanAttributes.HTTP_STATUS_CODE, response.status_code - ) - _retry_span.end() - return response + _span = tracer.start_span(span_name, _context) + return _span def _get_current_options(self, request: httpx.Request) -> RetryHandlerOption: """Returns the options to use for the request.Overries default options if From 3c11db317a4649cb5ac80a2d79c14da5630e6515 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 14:45:07 +0300 Subject: [PATCH 27/60] Refactor middleware to have a common interface create the span from the request with a common interface to reduce code duplication Signed-off-by: Martin Musale --- kiota_http/middleware/middleware.py | 19 +++++ .../parameters_name_decoding_handler.py | 13 +-- kiota_http/middleware/redirect_handler.py | 59 ++++++-------- kiota_http/middleware/retry_handler.py | 79 ++++++++----------- kiota_http/middleware/url_replace_handler.py | 28 +++---- kiota_http/middleware/user_agent_handler.py | 36 ++++----- 6 files changed, 104 insertions(+), 130 deletions(-) diff --git a/kiota_http/middleware/middleware.py b/kiota_http/middleware/middleware.py index dd67c89..0c7b4f4 100644 --- a/kiota_http/middleware/middleware.py +++ b/kiota_http/middleware/middleware.py @@ -1,8 +1,15 @@ import ssl +from typing import Optional import httpx +from opentelemetry import trace from urllib3 import PoolManager +from .._version import VERSION +from ..observability_options import ObservabilityOptions + +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) + class MiddlewarePipeline(): """MiddlewarePipeline, entry point of middleware @@ -55,3 +62,15 @@ async def send(self, request, transport): response.request = request return response return await self.next.send(request, transport) + + def _create_observability_span(self, request, span_name: str) -> trace.Span: + """Gets the parent_span from the request options and creates a new span. + If no parent_span is found, we try to get the current span.""" + _span = None + if options := getattr(request, "options", None): + if parent_span := options.get("parent_span", None): + _context = trace.set_span_in_context(parent_span) + _span = tracer.start_span(span_name, _context) + if _span is None: + _span = trace.get_current_span() + return _span diff --git a/kiota_http/middleware/parameters_name_decoding_handler.py b/kiota_http/middleware/parameters_name_decoding_handler.py index c13cc16..7e849ff 100644 --- a/kiota_http/middleware/parameters_name_decoding_handler.py +++ b/kiota_http/middleware/parameters_name_decoding_handler.py @@ -2,14 +2,10 @@ import httpx from kiota_abstractions.request_option import RequestOption -from opentelemetry import trace -from .._version import VERSION -from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import ParametersNameDecodingHandlerOption -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) PARAMETERS_NAME_DECODING_KEY = "com.microsoft.kiota.handler.parameters_name_decoding.enable" @@ -42,12 +38,9 @@ async def send( Response: The response object. """ current_options = self._get_current_options(request) - if options := getattr(request, "options", None): - if parent_span := options.get("parent_span", None): - _context = trace.set_span_in_context(parent_span) - span = tracer.start_span("ParametersNameDecodingHandler_send", context=_context) - span.set_attribute(PARAMETERS_NAME_DECODING_KEY, current_options.enabled) - span.end() + span = self._create_observability_span(request, "ParametersNameDecodingHandler_send") + span.set_attribute(PARAMETERS_NAME_DECODING_KEY, current_options.enabled) + span.end() updated_url: str = str(request.url) # type: ignore if all( diff --git a/kiota_http/middleware/redirect_handler.py b/kiota_http/middleware/redirect_handler.py index 58fdea2..3a1e915 100644 --- a/kiota_http/middleware/redirect_handler.py +++ b/kiota_http/middleware/redirect_handler.py @@ -2,18 +2,14 @@ import httpx from kiota_abstractions.request_option import RequestOption -from opentelemetry import trace from opentelemetry.semconv.trace import SpanAttributes from .._exceptions import RedirectError -from .._version import VERSION -from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import RedirectHandlerOption REDIRECT_ENABLE_KEY = "com.microsoft.kiota.handler.redirect.enable" REDIRECT_COUNT_KEY = "com.microsoft.kiota.handler.redirect.count" -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) class RedirectHandler(BaseMiddleware): @@ -68,36 +64,31 @@ async def send( """Sends the http request object to the next middleware or redirects the request if necessary. """ - if options := getattr(request, "options", None): - if parent_span := options.get("parent_span", None): - _context = trace.set_span_in_context(parent_span) - _enable_span = tracer.start_span("RedirectHandler_send", _context) - current_options = self._get_current_options(request) - _enable_span.set_attribute(REDIRECT_ENABLE_KEY, True) - _enable_span.end() - - retryable = True - _redirect_span = tracer.start_span( - f"RedirectHandler_send - redirect {len(self.history)}", _context - ) - while retryable: - response = await super().send(request, transport) - _redirect_span.set_attribute( - SpanAttributes.HTTP_STATUS_CODE, response.status_code - ) - redirect_location = self.get_redirect_location(response) - if redirect_location and current_options.should_redirect: - current_options.max_redirect -= 1 - retryable = self.increment(response, current_options.max_redirect) - _redirect_span.set_attribute(REDIRECT_COUNT_KEY, len(self.history)) - new_request = self._build_redirect_request(request, response) - request = new_request - continue - response.history = self.history - _redirect_span.end() - exc = RedirectError(f"Too many redirects. {response.history}") - parent_span.record_exception(exc) - raise exc + _enable_span = self._create_observability_span(request, "RedirectHandler_send") + current_options = self._get_current_options(request) + _enable_span.set_attribute(REDIRECT_ENABLE_KEY, True) + _enable_span.end() + + retryable = True + _redirect_span = self._create_observability_span( + request, f"RedirectHandler_send - redirect {len(self.history)}" + ) + while retryable: + response = await super().send(request, transport) + _redirect_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + redirect_location = self.get_redirect_location(response) + if redirect_location and current_options.should_redirect: + current_options.max_redirect -= 1 + retryable = self.increment(response, current_options.max_redirect) + _redirect_span.set_attribute(REDIRECT_COUNT_KEY, len(self.history)) + new_request = self._build_redirect_request(request, response) + request = new_request + continue + response.history = self.history + exc = RedirectError(f"Too many redirects. {response.history}") + _redirect_span.record_exception(exc) + _redirect_span.end() + raise exc return response def _get_current_options(self, request: httpx.Request) -> RedirectHandlerOption: diff --git a/kiota_http/middleware/retry_handler.py b/kiota_http/middleware/retry_handler.py index aa6986d..041f75a 100644 --- a/kiota_http/middleware/retry_handler.py +++ b/kiota_http/middleware/retry_handler.py @@ -6,16 +6,11 @@ import httpx from kiota_abstractions.request_option import RequestOption -from opentelemetry import trace from opentelemetry.semconv.trace import SpanAttributes -from .._version import VERSION -from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import RetryHandlerOption -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) - class RetryHandler(BaseMiddleware): """ @@ -76,51 +71,41 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport retry_count = 0 _span = self._create_observability_span(request, "RetryHandler_send") - if _span is not None: - current_options = self._get_current_options(request) - _span.set_attribute("com.microsoft.kiota.handler.retry.enable", True) - _span.end() - retry_valid = current_options.should_retry - _retry_span = self._create_observability_span( - request, f"RetryHandler_send - attempt {retry_count}" - ) - while retry_valid: - start_time = time.time() + current_options = self._get_current_options(request) + _span.set_attribute("com.microsoft.kiota.handler.retry.enable", True) + _span.end() + retry_valid = current_options.should_retry + _retry_span = self._create_observability_span( + request, f"RetryHandler_send - attempt {retry_count}" + ) + while retry_valid: + start_time = time.time() + response = await super().send(request, transport) + _retry_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + # check that max retries has not been hit + retry_valid = self.check_retry_valid(retry_count, current_options) + + # Get the delay time between retries + delay = self.get_delay_time(retry_count, response) + + # Check if the request needs to be retried based on the response method + # and status code + should_retry = self.should_retry(request, current_options, response) + if all([should_retry, retry_valid, delay < current_options.max_delay]): + time.sleep(delay) + end_time = time.time() + current_options.max_delay -= (end_time - start_time) + # 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) + continue + break + if response is None: response = await super().send(request, transport) - _retry_span.set_attributes(SpanAttributes.HTTP_STATUS_CODE, response.status_code) - # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_count, current_options) - - # Get the delay time between retries - delay = self.get_delay_time(retry_count, response) - - # Check if the request needs to be retried based on the response method - # and status code - should_retry = self.should_retry(request, current_options, response) - if all(should_retry, retry_valid, delay < current_options.max_delay): - time.sleep(delay) - end_time = time.time() - current_options.max_delay -= (end_time - start_time) - # 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) - continue - break - if response is None: - response = await super().send(request, transport) - _retry_span.end() + _retry_span.end() return response - def _create_observability_span(request: httpx.Request, span_name: str) -> Optional[trace.Span]: - """Gets the parent_span from the request options and creates a new span.""" - _span: trace.Span = None - if options := getattr(request, "options", None): - if parent_span := options.get("parent_span", None): - _context = trace.set_span_in_context(parent_span) - _span = tracer.start_span(span_name, _context) - return _span - def _get_current_options(self, request: httpx.Request) -> RetryHandlerOption: """Returns the options to use for the request.Overries default options if request options are passed. diff --git a/kiota_http/middleware/url_replace_handler.py b/kiota_http/middleware/url_replace_handler.py index 373f13a..8dcadca 100644 --- a/kiota_http/middleware/url_replace_handler.py +++ b/kiota_http/middleware/url_replace_handler.py @@ -1,15 +1,10 @@ import httpx from kiota_abstractions.request_option import RequestOption -from opentelemetry import trace from opentelemetry.semconv.trace import SpanAttributes -from .._version import VERSION -from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import UrlReplaceHandlerOption -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) - class UrlReplaceHandler(BaseMiddleware): @@ -36,20 +31,17 @@ async def send( Returns: Response: The response object. """ - response: httpx.Response - if options := getattr(request, "options", None): - if parent_span := options.get("parent_span", None): - _context = trace.set_span_in_context(parent_span) - _enable_span = tracer.start_span("UrlReplaceHandler_send", _context) - _enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True) - current_options = self._get_current_options(request) + response = None + _enable_span = self._create_observability_span(request, "UrlReplaceHandler_send") + _enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True) + current_options = self._get_current_options(request) - url_string: str = str(request.url) #type: ignore - 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)) - response = await super().send(request, transport) - _enable_span.end() + url_string: str = str(request.url) #type: ignore + 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)) + response = await super().send(request, transport) + _enable_span.end() return response def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOption: diff --git a/kiota_http/middleware/user_agent_handler.py b/kiota_http/middleware/user_agent_handler.py index 548e103..cc7559a 100644 --- a/kiota_http/middleware/user_agent_handler.py +++ b/kiota_http/middleware/user_agent_handler.py @@ -1,15 +1,10 @@ from httpx import AsyncBaseTransport, Request, Response from kiota_abstractions.request_option import RequestOption -from opentelemetry import trace from opentelemetry.semconv.trace import SpanAttributes -from .._version import VERSION -from ..observability_options import ObservabilityOptions from .middleware import BaseMiddleware from .options import UserAgentHandlerOption -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) - class UserAgentHandler(BaseMiddleware): """ @@ -25,20 +20,19 @@ async def send(self, request: Request, transport: AsyncBaseTransport) -> Respons Checks if the request has a User-Agent header and updates it if the platform config allows. """ - if options := getattr(request, "options", None): - if parent_span := options.get("parent_span", None): - _context = trace.set_span_in_context(parent_span) - _span = tracer.start_span("UserAgentHandler_send", _context) - if self.options and self.options.is_enabled: - _span.set_attribute("com.microsoft.kiota.handler.useragent.enable", True) - value = f"{self.options.product_name}/{self.options.product_version}" - - user_agent = request.headers.get("User-Agent", "") - if not user_agent: - request.headers.update({"User-Agent": value}) - else: - if value not in user_agent: - request.headers.update({"User-Agent": f"{user_agent} {value}"}) - _span.end() - + _span = self._create_observability_span(request, "UserAgentHandler_send") + if self.options and self.options.is_enabled: + _span.set_attribute("com.microsoft.kiota.handler.useragent.enable", True) + value = f"{self.options.product_name}/{self.options.product_version}" + self._update_user_agent(request, value) + _span.end() return await super().send(request, transport) + + def _update_user_agent(self, request: Request, value: str): + """Updates the values of the User-Agent header.""" + user_agent = request.headers.get("User-Agent", "") + if not user_agent: + request.headers.update({"User-Agent": value}) + else: + if value not in user_agent: + request.headers.update({"User-Agent": f"{user_agent} {value}"}) From cdc44614305150d9589a18463e771f1ac8416c65 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 23:33:14 +0300 Subject: [PATCH 28/60] Track the parent_span in the base middleware Signed-off-by: Martin Musale --- kiota_http/middleware/middleware.py | 2 ++ tests/middleware_tests/test_base_middleware.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/middleware_tests/test_base_middleware.py diff --git a/kiota_http/middleware/middleware.py b/kiota_http/middleware/middleware.py index 0c7b4f4..a363a32 100644 --- a/kiota_http/middleware/middleware.py +++ b/kiota_http/middleware/middleware.py @@ -52,6 +52,7 @@ class BaseMiddleware(): def __init__(self): self.next = None + self.parent_span = None async def send(self, request, transport): if self.next is None: @@ -69,6 +70,7 @@ def _create_observability_span(self, request, span_name: str) -> trace.Span: _span = None if options := getattr(request, "options", None): if parent_span := options.get("parent_span", None): + self.parent_span = parent_span _context = trace.set_span_in_context(parent_span) _span = tracer.start_span(span_name, _context) if _span is None: diff --git a/tests/middleware_tests/test_base_middleware.py b/tests/middleware_tests/test_base_middleware.py new file mode 100644 index 0000000..53e0bc3 --- /dev/null +++ b/tests/middleware_tests/test_base_middleware.py @@ -0,0 +1,17 @@ +"""Test the BaseMiddleware class.""" +from opentelemetry import trace + +from kiota_http.middleware import BaseMiddleware + + +def test_next_is_none(): + """Ensure there is no next middleware.""" + 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() + span = middleware._create_observability_span(request_info, "test_span_created") + assert isinstance(span, trace.Span) + assert middleware.parent_span is None From 1b076fcbb0a80adb04ac73c0e0f37ed0d0907103 Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 23:34:08 +0300 Subject: [PATCH 29/60] Fix sorting of imports Signed-off-by: Martin Musale --- tests/middleware_tests/test_user_agent_handler.py | 3 ++- tests/test_kiota_client_factory.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/middleware_tests/test_user_agent_handler.py b/tests/middleware_tests/test_user_agent_handler.py index e3a5843..c2f0e47 100644 --- a/tests/middleware_tests/test_user_agent_handler.py +++ b/tests/middleware_tests/test_user_agent_handler.py @@ -1,6 +1,7 @@ import pytest -from kiota_http.middleware.options.user_agent_handler_option import UserAgentHandlerOption + from kiota_http._version import VERSION +from kiota_http.middleware.options.user_agent_handler_option import UserAgentHandlerOption def test_no_config(): diff --git a/tests/test_kiota_client_factory.py b/tests/test_kiota_client_factory.py index f46f015..3d5df80 100644 --- a/tests/test_kiota_client_factory.py +++ b/tests/test_kiota_client_factory.py @@ -3,8 +3,12 @@ from kiota_http.kiota_client_factory import KiotaClientFactory from kiota_http.middleware import ( - AsyncKiotaTransport, MiddlewarePipeline, ParametersNameDecodingHandler, RedirectHandler, - RetryHandler, UrlReplaceHandler + AsyncKiotaTransport, + MiddlewarePipeline, + ParametersNameDecodingHandler, + RedirectHandler, + RetryHandler, + UrlReplaceHandler, ) from kiota_http.middleware.options import RedirectHandlerOption, RetryHandlerOption from kiota_http.middleware.user_agent_handler import UserAgentHandler From d0f42dee78d4ec4afaa3cc20b34bb42a059e0f1b Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 23:34:35 +0300 Subject: [PATCH 30/60] Add a test to check if the methods that setup o11y are called Signed-off-by: Martin Musale --- tests/test_httpx_request_adapter.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_httpx_request_adapter.py b/tests/test_httpx_request_adapter.py index 9b6c37f..584a7c9 100644 --- a/tests/test_httpx_request_adapter.py +++ b/tests/test_httpx_request_adapter.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import httpx import pytest from asyncmock import AsyncMock @@ -8,6 +10,7 @@ ParseNodeFactoryRegistry, SerializationWriterFactoryRegistry, ) +from opentelemetry import trace from kiota_http.httpx_request_adapter import HttpxRequestAdapter from kiota_http.middleware.options import ResponseHandlerOption @@ -265,3 +268,19 @@ async def test_convert_to_native_async(request_adapter, request_info): request_info.content = bytes('hello world', 'utf_8') req = await request_adapter.convert_to_native_async(request_info) assert isinstance(req, httpx.Request) + +@pytest.mark.asyncio +async def test_observability(request_adapter, request_info, mock_user_response, mock_user): + """Ensures the otel tracer and created spans are set and called correctly.""" + request_adapter.get_http_response_message = AsyncMock(return_value=mock_user_response) + request_adapter.get_root_parse_node = AsyncMock(return_value=mock_user) + resp = await request_adapter.get_http_response_message(request_info) + assert resp.headers.get("content-type") == 'application/json' + + with patch("kiota_http.httpx_request_adapter.HttpxRequestAdapter.start_tracing_span") as start_tracing_span: + final_result = await request_adapter.send_async(request_info, MockResponseObject, {}) + assert start_tracing_span is not None + # check if the send_async span is created + start_tracing_span.assert_called_once_with(request_info, "send_async") + assert final_result.display_name == mock_user.display_name + assert not trace.get_current_span().is_recording() From f8490d7bd8129e7b734b40d974b8f38f0c4437fa Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Wed, 2 Aug 2023 23:37:39 +0300 Subject: [PATCH 31/60] Fix indentation of code to fix unreachable code bug Signed-off-by: Martin Musale --- kiota_http/middleware/retry_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kiota_http/middleware/retry_handler.py b/kiota_http/middleware/retry_handler.py index 041f75a..f51825d 100644 --- a/kiota_http/middleware/retry_handler.py +++ b/kiota_http/middleware/retry_handler.py @@ -101,8 +101,8 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport _retry_span.set_attribute(SpanAttributes.HTTP_RETRY_COUNT, retry_count) continue break - if response is None: - response = await super().send(request, transport) + if response is None: + response = await super().send(request, transport) _retry_span.end() return response From 6ca816895b96c2ebee71609800337d5aec8b54dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:40:36 +0000 Subject: [PATCH 32/60] Bump platformdirs from 3.9.1 to 3.10.0 Bumps [platformdirs](https://github.com/platformdirs/platformdirs) from 3.9.1 to 3.10.0. - [Release notes](https://github.com/platformdirs/platformdirs/releases) - [Changelog](https://github.com/platformdirs/platformdirs/blob/main/CHANGES.rst) - [Commits](https://github.com/platformdirs/platformdirs/compare/3.9.1...3.10.0) --- updated-dependencies: - dependency-name: platformdirs dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e528ae7..fd88dcf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -40,7 +40,7 @@ mypy-extensions==1.0.0 packaging==23.1 -platformdirs==3.9.1 +platformdirs==3.10.0 pluggy==1.2.0 From c9fa008e7868d74b75cab105f2e9edbe48ee3d2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:53:11 +0000 Subject: [PATCH 33/60] Bump microsoft-kiota-abstractions from 0.6.0 to 0.7.0 Bumps [microsoft-kiota-abstractions](https://github.com/microsoft/kiota) from 0.6.0 to 0.7.0. - [Release notes](https://github.com/microsoft/kiota/releases) - [Changelog](https://github.com/microsoft/kiota/blob/main/CHANGELOG.md) - [Commits](https://github.com/microsoft/kiota/compare/v0.6.0...v0.7.0) --- updated-dependencies: - dependency-name: microsoft-kiota-abstractions dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fd88dcf..72b8b59 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -88,7 +88,7 @@ httpx[http2]==0.24.1 hyperframe==6.0.1 -microsoft-kiota-abstractions==0.6.0 +microsoft-kiota-abstractions==0.7.0 sniffio==1.3.0 From afceee895be280e5c2e6b5ec0a3fffbe4c79442d Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Jul 2023 15:56:29 +0300 Subject: [PATCH 34/60] Initialize backingstorefactorysingleton with the provided factory --- kiota_http/httpx_request_adapter.py | 90 +++++++++++++++++++---------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index a6d6703..8f71721 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -43,7 +43,8 @@ DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" REQUEST_IS_NULL = RequestError("Request info cannot be null") -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) +tracer = trace.get_tracer( + ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) class HttpxRequestAdapter(RequestAdapter, Generic[ModelType]): @@ -122,7 +123,8 @@ def start_tracing_span(self, request_info: RequestInformation, method: str) -> t The parent span. """ name_handler = ParametersNameDecodingHandler() - uri_template = name_handler.decode_uri_encoded_string(request_info.url_template) + uri_template = name_handler.decode_uri_encoded_string( + request_info.url_template) parent_span_name = f"{method} - {uri_template}" span = tracer.start_span(parent_span_name) return span @@ -168,9 +170,11 @@ async def send_async( if self._should_return_none(response): return None root_node = await self.get_root_parse_node(response, parent_span, parent_span) - _deserialized_span = self._start_local_tracing_span("get_object_value", parent_span) + _deserialized_span = self._start_local_tracing_span( + "get_object_value", parent_span) value = root_node.get_object_value(parsable_factory) - parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + parent_span.set_attribute( + DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) _deserialized_span.end() return value finally: @@ -194,7 +198,8 @@ async def send_collection_async( Returns: ModelType: the deserialized response model collection. """ - parent_span = self.start_tracing_span(request_info, "send_collection_async") + parent_span = self.start_tracing_span( + request_info, "send_collection_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -214,8 +219,10 @@ async def send_collection_async( "get_collection_of_object_values", parent_span ) root_node = await self.get_root_parse_node(response, parent_span, parent_span) - result = root_node.get_collection_of_object_values(parsable_factory) - parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) + result = root_node.get_collection_of_object_values( + parsable_factory) + parent_span.set_attribute( + DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) _deserialized_span.end() return result finally: @@ -239,7 +246,8 @@ async def send_collection_of_primitive_async( Returns: Optional[List[ModelType]]: he deserialized response model collection. """ - parent_span = self.start_tracing_span(request_info, "send_collection_of_primitive_async") + parent_span = self.start_tracing_span( + request_info, "send_collection_of_primitive_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -261,8 +269,10 @@ async def send_collection_of_primitive_async( "get_collection_of_primitive_values", parent_span ) root_node = await self.get_root_parse_node(response, parent_span, parent_span) - values = root_node.get_collection_of_primitive_values(response_type) - parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, values.__class__.__name__) + values = root_node.get_collection_of_primitive_values( + response_type) + parent_span.set_attribute( + DESERIALIZED_MODEL_NAME_KEY, values.__class__.__name__) _deserialized_span.end() return values finally: @@ -286,7 +296,8 @@ async def send_primitive_async( Returns: ResponseType: the deserialized primitive response model. """ - parent_span = self.start_tracing_span(request_info, "send_primitive_async") + parent_span = self.start_tracing_span( + request_info, "send_primitive_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -304,7 +315,8 @@ async def send_primitive_async( return None if response_type == "bytes": return response.content - _deserialized_span = self._start_local_tracing_span("get_root_parse_node", parent_span) + _deserialized_span = self._start_local_tracing_span( + "get_root_parse_node", parent_span) root_node = await self.get_root_parse_node(response, parent_span, parent_span) value = None if response_type == "str": @@ -318,7 +330,8 @@ async def send_primitive_async( if response_type == "datetime": value = root_node.get_datetime_value() if value: - parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + parent_span.set_attribute( + DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) _deserialized_span.end() return value @@ -340,7 +353,8 @@ async def send_no_response_content_async( error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. """ - parent_span = self.start_tracing_span(request_info, "send_no_response_content_async") + parent_span = self.start_tracing_span( + request_info, "send_no_response_content_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -371,9 +385,11 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto ) ) if not any([self._serialization_writer_factory, self._parse_node_factory]): - raise BackingstoreError("Unable to enable backing store") + raise Exception("Unable to enable backing store") + if backing_store_factory: - BackingStoreFactorySingleton.__instance = backing_store_factory + BackingStoreFactorySingleton( + backing_store_factory=backing_store_factory) async def get_root_parse_node( self, @@ -381,13 +397,15 @@ async def get_root_parse_node( parent_span: trace.Span, attribute_span: trace.Span, ) -> ParseNode: - span = self._start_local_tracing_span("get_root_parse_node", parent_span) + span = self._start_local_tracing_span( + "get_root_parse_node", parent_span) try: payload = response.content response_content_type = self.get_response_content_type(response) if not response_content_type: - raise DeserializationError("No response content type found for deserialization") + raise DeserializationError( + "No response content type found for deserialization") return self._parse_node_factory.get_root_parse_node(response_content_type, payload) finally: span.end() @@ -416,8 +434,10 @@ async def throw_failed_responses( response_status_code_str = str(response_status_code) response_headers = response.headers - _throw_failed_resp_span.set_attribute("status", response_status_code) - _throw_failed_resp_span.set_attribute(ERROR_MAPPING_FOUND_KEY, bool(error_map)) + _throw_failed_resp_span.set_attribute( + "status", response_status_code) + _throw_failed_resp_span.set_attribute( + ERROR_MAPPING_FOUND_KEY, bool(error_map)) if not error_map: exc = APIError( "The server returned an unexpected status code and no error class is registered" @@ -426,9 +446,11 @@ async def throw_failed_responses( response_headers, ) # set this or ignore as description in set_status? - _throw_failed_resp_span.set_attribute("status_message", "received_error_response") + _throw_failed_resp_span.set_attribute( + "status_message", "received_error_response") # TODO: set status for just this span or the parent as well? - _throw_failed_resp_span.set_status(trace.StatusCode.ERROR, str(exc)) + _throw_failed_resp_span.set_status( + trace.StatusCode.ERROR, str(exc)) attribute_span.record_exception(exc) raise exc @@ -444,7 +466,8 @@ async def throw_failed_responses( ) attribute_span.record_exception(exc) raise exc - _throw_failed_resp_span.set_attribute("status_message", "received_error_response") + _throw_failed_resp_span.set_attribute( + "status_message", "received_error_response") error_class = None if response_status_code_str in error_map: @@ -460,7 +483,8 @@ async def throw_failed_responses( attribute_span.set_attribute(ERROR_BODY_FOUND_KEY, bool(root_node)) _get_obj_ctx = trace.set_span_in_context(_throw_failed_resp_span) - _get_obj_span = tracer.start_span("get_object_value", context=_get_obj_ctx) + _get_obj_span = tracer.start_span( + "get_object_value", context=_get_obj_ctx) error = root_node.get_object_value(error_class) if isinstance(error, APIError): @@ -491,20 +515,24 @@ async def get_http_response_message( request_info, _get_http_resp_span, parent_span ) resp = await self._http_client.send(request) - parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code) + parent_span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, resp.status_code) if http_version := resp.http_version: parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, 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( + SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 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_content_type", content_type) _get_http_resp_span.end() return resp def get_response_handler(self, request_info: RequestInformation) -> Any: - response_handler_option = request_info.request_options.get(ResponseHandlerOption.get_key()) + response_handler_option = request_info.request_options.get( + ResponseHandlerOption.get_key()) if response_handler_option: return response_handler_option.response_handler return None @@ -547,7 +575,8 @@ 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( + {SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) if content_type := request.headers.get("Content-Type", None): otel_attributes.update({"http.request_content_type": content_type}) @@ -558,7 +587,8 @@ def get_request_from_request_information( return request async def convert_to_native_async(self, request_info: RequestInformation) -> httpx.Request: - parent_span = self.start_tracing_span(request_info, "convert_to_native_async") + parent_span = self.start_tracing_span( + request_info, "convert_to_native_async") try: if request_info is None: exc = ValueError("request information must be provided") From 93ec1dfffae1e6d1417ffd9a9698be51f6be14c0 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Jul 2023 16:02:43 +0300 Subject: [PATCH 35/60] Remove pipenv dependency --- .github/workflows/build_publish.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build_publish.yml b/.github/workflows/build_publish.yml index c41d6d0..0c18925 100644 --- a/.github/workflows/build_publish.yml +++ b/.github/workflows/build_publish.yml @@ -23,23 +23,22 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pipenv - pipenv install -r requirements-dev.txt + pip install -r requirements-dev.txt - name: Check code format run: | - pipenv run yapf -dr kiota_http + yapf -dr kiota_http - name: Check import order run: | - pipenv run isort kiota_http + isort kiota_http - name: Lint with Pylint run: | - pipenv run pylint kiota_http --disable=W --rcfile=.pylintrc + pylint kiota_http --disable=W --rcfile=.pylintrc - name: Static type checking with Mypy run: | - pipenv run mypy kiota_http + mypy kiota_http - name: Run tests with Pytest run: | - pipenv run pytest + pytest publish: name: Publish distribution to PyPI From 7631718d693da998297550eeec238dcc730ab526 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Jul 2023 16:06:02 +0300 Subject: [PATCH 36/60] Bump package version and update CHANGELOG --- CHANGELOG.md | 3 ++- kiota_http/_version.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b698e..286e66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +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). -## [0.4.5] - 2023-07-18 +## [0.5.0] - 2023-07-27 ### Added - Added a translator method to change a `RequestInformation` object into a HTTPX client request object. +- Enabled backing store support ### Changed diff --git a/kiota_http/_version.py b/kiota_http/_version.py index b3d3b1b..ca2ac7e 100644 --- a/kiota_http/_version.py +++ b/kiota_http/_version.py @@ -1 +1 @@ -VERSION: str = '0.4.5' +VERSION: str = '0.5.0' From 933253986f02185b001a6206155879741dd8fe93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 17:53:08 +0000 Subject: [PATCH 37/60] Bump mypy from 1.4.1 to 1.5.0 Bumps [mypy](https://github.com/python/mypy) from 1.4.1 to 1.5.0. - [Commits](https://github.com/python/mypy/compare/v1.4.1...v1.5.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 72b8b59..e989988 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -34,7 +34,7 @@ mccabe==0.7.0 mock==5.1.0 -mypy==1.4.1 +mypy==1.5.0 mypy-extensions==1.0.0 From c1f1440a85830200e54365946acea7b664b873f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 17:53:14 +0000 Subject: [PATCH 38/60] Bump microsoft-kiota-abstractions from 0.7.0 to 0.7.1 Bumps [microsoft-kiota-abstractions](https://github.com/microsoft/kiota) from 0.7.0 to 0.7.1. - [Release notes](https://github.com/microsoft/kiota/releases) - [Changelog](https://github.com/microsoft/kiota/blob/main/CHANGELOG.md) - [Commits](https://github.com/microsoft/kiota/compare/v0.7.0...v0.7.1) --- updated-dependencies: - dependency-name: microsoft-kiota-abstractions dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e989988..7d255f3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -88,7 +88,7 @@ httpx[http2]==0.24.1 hyperframe==6.0.1 -microsoft-kiota-abstractions==0.7.0 +microsoft-kiota-abstractions==0.7.1 sniffio==1.3.0 From 3b12b757e66c7885daac3cb1565bf635f52ff22d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 17:48:26 +0000 Subject: [PATCH 39/60] Bump exceptiongroup from 1.1.2 to 1.1.3 Bumps [exceptiongroup](https://github.com/agronholm/exceptiongroup) from 1.1.2 to 1.1.3. - [Changelog](https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst) - [Commits](https://github.com/agronholm/exceptiongroup/compare/1.1.2...1.1.3) --- updated-dependencies: - dependency-name: exceptiongroup dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7d255f3..dfeaacb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,7 +16,7 @@ dill==0.3.7 docutils==0.20.1 -exceptiongroup==1.1.2 +exceptiongroup==1.1.3 flit==3.9.0 From 78bc4a468c452354d9cacf118b78fbc526d9de7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 17:48:37 +0000 Subject: [PATCH 40/60] Bump coverage[toml] from 7.2.7 to 7.3.0 Bumps [coverage[toml]](https://github.com/nedbat/coveragepy) from 7.2.7 to 7.3.0. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.2.7...7.3.0) --- updated-dependencies: - dependency-name: coverage[toml] dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index dfeaacb..8ef48a7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ charset-normalizer==3.2.0 colorama==0.4.6 -coverage[toml]==7.2.7 +coverage[toml]==7.3.0 dill==0.3.7 From 907a0c5816d30922a4297ab7943915aed7ff47c0 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 19:14:50 +0300 Subject: [PATCH 41/60] Add telemetry for send_async method Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 8f71721..9550890 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -36,6 +36,13 @@ ResponseType = Union[str, int, float, bool, datetime, bytes] ModelType = TypeVar("ModelType", bound=Parsable) +RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" +ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" +ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" +DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" + +tracer = trace.get_tracer( + ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" @@ -385,7 +392,10 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto ) ) if not any([self._serialization_writer_factory, self._parse_node_factory]): - raise Exception("Unable to enable backing store") + raise BackingstoreException("Unable to enable backing store") + if backing_store_factory: + BackingStoreFactorySingleton( + backing_store_factory=backing_store_factory) if backing_store_factory: BackingStoreFactorySingleton( From e493c150064cc5210caa9b20d5ceb46e681e106b Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 31 Jul 2023 20:43:08 +0300 Subject: [PATCH 42/60] Fix code smells and rename exceptions Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 9550890..e249bae 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -36,10 +36,12 @@ ResponseType = Union[str, int, float, bool, datetime, bytes] ModelType = TypeVar("ModelType", bound=Parsable) + RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" +REQUEST_IS_NULL = RequestError("Request info cannot be null") tracer = trace.get_tracer( ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) @@ -392,7 +394,7 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto ) ) if not any([self._serialization_writer_factory, self._parse_node_factory]): - raise BackingstoreException("Unable to enable backing store") + raise BackingstoreError("Unable to enable backing store") if backing_store_factory: BackingStoreFactorySingleton( backing_store_factory=backing_store_factory) @@ -414,8 +416,7 @@ async def get_root_parse_node( payload = response.content response_content_type = self.get_response_content_type(response) if not response_content_type: - raise DeserializationError( - "No response content type found for deserialization") + raise DeserializationError("No response content type found for deserialization") return self._parse_node_factory.get_root_parse_node(response_content_type, payload) finally: span.end() From 363000bb04925c577163f53e7ee18f83dd68b102 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 16 Aug 2023 13:18:00 +0300 Subject: [PATCH 43/60] Enable tracing if options is enabled Signed-off-by: Musale Martin --- .../parameters_name_decoding_handler.py | 7 ++++-- kiota_http/middleware/url_replace_handler.py | 22 +++++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/kiota_http/middleware/parameters_name_decoding_handler.py b/kiota_http/middleware/parameters_name_decoding_handler.py index 7e849ff..0dc772a 100644 --- a/kiota_http/middleware/parameters_name_decoding_handler.py +++ b/kiota_http/middleware/parameters_name_decoding_handler.py @@ -38,8 +38,11 @@ async def send( Response: The response object. """ current_options = self._get_current_options(request) - span = self._create_observability_span(request, "ParametersNameDecodingHandler_send") - span.set_attribute(PARAMETERS_NAME_DECODING_KEY, current_options.enabled) + span = self._create_observability_span( + request, "ParametersNameDecodingHandler_send") + if current_options.enabled: + span.set_attribute(PARAMETERS_NAME_DECODING_KEY, + current_options.enabled) span.end() updated_url: str = str(request.url) # type: ignore diff --git a/kiota_http/middleware/url_replace_handler.py b/kiota_http/middleware/url_replace_handler.py index 8dcadca..8a41f15 100644 --- a/kiota_http/middleware/url_replace_handler.py +++ b/kiota_http/middleware/url_replace_handler.py @@ -21,7 +21,7 @@ def __init__(self, options: RequestOption = UrlReplaceHandlerOption()): async def send( self, request: httpx.Request, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: #type: ignore + ) -> httpx.Response: # type: ignore """To execute the current middleware Args: @@ -32,14 +32,18 @@ async def send( Response: The response object. """ response = None - _enable_span = self._create_observability_span(request, "UrlReplaceHandler_send") - _enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True) - current_options = self._get_current_options(request) + _enable_span = self._create_observability_span( + request, "UrlReplaceHandler_send") + if self.options and self.options.is_enabled: + _enable_span.set_attribute( + "com.microsoft.kiota.handler.url_replacer.enable", True) + current_options = self._get_current_options(request) - url_string: str = str(request.url) #type: ignore - 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)) + url_string: str = str(request.url) # type: ignore + 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)) response = await super().send(request, transport) _enable_span.end() return response @@ -54,7 +58,7 @@ def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOptio Returns: UrlReplaceHandlerOption: The options to be used. """ - current_options =request.options.get( # type:ignore + current_options = request.options.get( # type:ignore UrlReplaceHandlerOption.get_key(), self.options ) return current_options From aeb77c76658fbb460b3eff32cb7105516edced3d Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 16 Aug 2023 13:21:29 +0300 Subject: [PATCH 44/60] Enable tracing if options is enabled Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 3 +-- .../middleware/parameters_name_decoding_handler.py | 6 ++---- kiota_http/middleware/url_replace_handler.py | 9 +++------ 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index e249bae..cbbd0d9 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -396,8 +396,7 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto if not any([self._serialization_writer_factory, self._parse_node_factory]): raise BackingstoreError("Unable to enable backing store") if backing_store_factory: - BackingStoreFactorySingleton( - backing_store_factory=backing_store_factory) + BackingStoreFactorySingleton(backing_store_factory=backing_store_factory) if backing_store_factory: BackingStoreFactorySingleton( diff --git a/kiota_http/middleware/parameters_name_decoding_handler.py b/kiota_http/middleware/parameters_name_decoding_handler.py index 0dc772a..6134844 100644 --- a/kiota_http/middleware/parameters_name_decoding_handler.py +++ b/kiota_http/middleware/parameters_name_decoding_handler.py @@ -38,11 +38,9 @@ async def send( Response: The response object. """ current_options = self._get_current_options(request) - span = self._create_observability_span( - request, "ParametersNameDecodingHandler_send") + span = self._create_observability_span(request, "ParametersNameDecodingHandler_send") if current_options.enabled: - span.set_attribute(PARAMETERS_NAME_DECODING_KEY, - current_options.enabled) + span.set_attribute(PARAMETERS_NAME_DECODING_KEY, current_options.enabled) span.end() updated_url: str = str(request.url) # type: ignore diff --git a/kiota_http/middleware/url_replace_handler.py b/kiota_http/middleware/url_replace_handler.py index 8a41f15..8c9a2c3 100644 --- a/kiota_http/middleware/url_replace_handler.py +++ b/kiota_http/middleware/url_replace_handler.py @@ -32,18 +32,15 @@ async def send( Response: The response object. """ response = None - _enable_span = self._create_observability_span( - request, "UrlReplaceHandler_send") + _enable_span = self._create_observability_span(request, "UrlReplaceHandler_send") if self.options and self.options.is_enabled: - _enable_span.set_attribute( - "com.microsoft.kiota.handler.url_replacer.enable", True) + _enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True) current_options = self._get_current_options(request) url_string: str = str(request.url) # type: ignore 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(SpanAttributes.HTTP_URL, str(request.url)) response = await super().send(request, transport) _enable_span.end() return response From 00096445653031c565e960e4f99e98b53267b849 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 16 Aug 2023 13:28:45 +0300 Subject: [PATCH 45/60] Fix formatting errors Signed-off-by: Musale Martin --- kiota_http/httpx_request_adapter.py | 87 ++++++++++------------------- 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index cbbd0d9..58f43c6 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -43,8 +43,7 @@ DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" REQUEST_IS_NULL = RequestError("Request info cannot be null") -tracer = trace.get_tracer( - ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" @@ -52,8 +51,7 @@ DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" REQUEST_IS_NULL = RequestError("Request info cannot be null") -tracer = trace.get_tracer( - ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) +tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) class HttpxRequestAdapter(RequestAdapter, Generic[ModelType]): @@ -132,8 +130,7 @@ def start_tracing_span(self, request_info: RequestInformation, method: str) -> t The parent span. """ name_handler = ParametersNameDecodingHandler() - uri_template = name_handler.decode_uri_encoded_string( - request_info.url_template) + uri_template = name_handler.decode_uri_encoded_string(request_info.url_template) parent_span_name = f"{method} - {uri_template}" span = tracer.start_span(parent_span_name) return span @@ -179,11 +176,9 @@ async def send_async( if self._should_return_none(response): return None root_node = await self.get_root_parse_node(response, parent_span, parent_span) - _deserialized_span = self._start_local_tracing_span( - "get_object_value", parent_span) + _deserialized_span = self._start_local_tracing_span("get_object_value", parent_span) value = root_node.get_object_value(parsable_factory) - parent_span.set_attribute( - DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) _deserialized_span.end() return value finally: @@ -207,8 +202,7 @@ async def send_collection_async( Returns: ModelType: the deserialized response model collection. """ - parent_span = self.start_tracing_span( - request_info, "send_collection_async") + parent_span = self.start_tracing_span(request_info, "send_collection_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -228,10 +222,8 @@ async def send_collection_async( "get_collection_of_object_values", parent_span ) root_node = await self.get_root_parse_node(response, parent_span, parent_span) - result = root_node.get_collection_of_object_values( - parsable_factory) - parent_span.set_attribute( - DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) + result = root_node.get_collection_of_object_values(parsable_factory) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, result.__class__.__name__) _deserialized_span.end() return result finally: @@ -255,8 +247,7 @@ async def send_collection_of_primitive_async( Returns: Optional[List[ModelType]]: he deserialized response model collection. """ - parent_span = self.start_tracing_span( - request_info, "send_collection_of_primitive_async") + parent_span = self.start_tracing_span(request_info, "send_collection_of_primitive_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -278,10 +269,8 @@ async def send_collection_of_primitive_async( "get_collection_of_primitive_values", parent_span ) root_node = await self.get_root_parse_node(response, parent_span, parent_span) - values = root_node.get_collection_of_primitive_values( - response_type) - parent_span.set_attribute( - DESERIALIZED_MODEL_NAME_KEY, values.__class__.__name__) + values = root_node.get_collection_of_primitive_values(response_type) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, values.__class__.__name__) _deserialized_span.end() return values finally: @@ -305,8 +294,7 @@ async def send_primitive_async( Returns: ResponseType: the deserialized primitive response model. """ - parent_span = self.start_tracing_span( - request_info, "send_primitive_async") + parent_span = self.start_tracing_span(request_info, "send_primitive_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -324,8 +312,7 @@ async def send_primitive_async( return None if response_type == "bytes": return response.content - _deserialized_span = self._start_local_tracing_span( - "get_root_parse_node", parent_span) + _deserialized_span = self._start_local_tracing_span("get_root_parse_node", parent_span) root_node = await self.get_root_parse_node(response, parent_span, parent_span) value = None if response_type == "str": @@ -339,8 +326,7 @@ async def send_primitive_async( if response_type == "datetime": value = root_node.get_datetime_value() if value: - parent_span.set_attribute( - DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) + parent_span.set_attribute(DESERIALIZED_MODEL_NAME_KEY, value.__class__.__name__) _deserialized_span.end() return value @@ -362,8 +348,7 @@ async def send_no_response_content_async( error_map (Dict[str, ParsableFactory]): the error dict to use in case of a failed request. """ - parent_span = self.start_tracing_span( - request_info, "send_no_response_content_async") + parent_span = self.start_tracing_span(request_info, "send_no_response_content_async") try: if not request_info: parent_span.record_exception(REQUEST_IS_NULL) @@ -399,8 +384,7 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto BackingStoreFactorySingleton(backing_store_factory=backing_store_factory) if backing_store_factory: - BackingStoreFactorySingleton( - backing_store_factory=backing_store_factory) + BackingStoreFactorySingleton(backing_store_factory=backing_store_factory) async def get_root_parse_node( self, @@ -408,8 +392,7 @@ async def get_root_parse_node( parent_span: trace.Span, attribute_span: trace.Span, ) -> ParseNode: - span = self._start_local_tracing_span( - "get_root_parse_node", parent_span) + span = self._start_local_tracing_span("get_root_parse_node", parent_span) try: payload = response.content @@ -444,10 +427,8 @@ async def throw_failed_responses( response_status_code_str = str(response_status_code) response_headers = response.headers - _throw_failed_resp_span.set_attribute( - "status", response_status_code) - _throw_failed_resp_span.set_attribute( - ERROR_MAPPING_FOUND_KEY, bool(error_map)) + _throw_failed_resp_span.set_attribute("status", response_status_code) + _throw_failed_resp_span.set_attribute(ERROR_MAPPING_FOUND_KEY, bool(error_map)) if not error_map: exc = APIError( "The server returned an unexpected status code and no error class is registered" @@ -456,11 +437,9 @@ async def throw_failed_responses( response_headers, ) # set this or ignore as description in set_status? - _throw_failed_resp_span.set_attribute( - "status_message", "received_error_response") + _throw_failed_resp_span.set_attribute("status_message", "received_error_response") # TODO: set status for just this span or the parent as well? - _throw_failed_resp_span.set_status( - trace.StatusCode.ERROR, str(exc)) + _throw_failed_resp_span.set_status(trace.StatusCode.ERROR, str(exc)) attribute_span.record_exception(exc) raise exc @@ -476,8 +455,7 @@ async def throw_failed_responses( ) attribute_span.record_exception(exc) raise exc - _throw_failed_resp_span.set_attribute( - "status_message", "received_error_response") + _throw_failed_resp_span.set_attribute("status_message", "received_error_response") error_class = None if response_status_code_str in error_map: @@ -493,8 +471,7 @@ async def throw_failed_responses( attribute_span.set_attribute(ERROR_BODY_FOUND_KEY, bool(root_node)) _get_obj_ctx = trace.set_span_in_context(_throw_failed_resp_span) - _get_obj_span = tracer.start_span( - "get_object_value", context=_get_obj_ctx) + _get_obj_span = tracer.start_span("get_object_value", context=_get_obj_ctx) error = root_node.get_object_value(error_class) if isinstance(error, APIError): @@ -525,24 +502,20 @@ async def get_http_response_message( request_info, _get_http_resp_span, parent_span ) resp = await self._http_client.send(request) - parent_span.set_attribute( - SpanAttributes.HTTP_STATUS_CODE, resp.status_code) + parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code) if http_version := resp.http_version: parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, 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(SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 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_content_type", content_type) _get_http_resp_span.end() return resp def get_response_handler(self, request_info: RequestInformation) -> Any: - response_handler_option = request_info.request_options.get( - ResponseHandlerOption.get_key()) + response_handler_option = request_info.request_options.get(ResponseHandlerOption.get_key()) if response_handler_option: return response_handler_option.response_handler return None @@ -585,8 +558,7 @@ 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({SpanAttributes.HTTP_REQUEST_CONTENT_LENGTH: content_length}) if content_type := request.headers.get("Content-Type", None): otel_attributes.update({"http.request_content_type": content_type}) @@ -597,8 +569,7 @@ def get_request_from_request_information( return request async def convert_to_native_async(self, request_info: RequestInformation) -> httpx.Request: - parent_span = self.start_tracing_span( - request_info, "convert_to_native_async") + parent_span = self.start_tracing_span(request_info, "convert_to_native_async") try: if request_info is None: exc = ValueError("request information must be provided") From 582a8eeb75c0bdcfe247907325c205334383362b Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Mon, 11 Sep 2023 20:04:43 +0300 Subject: [PATCH 46/60] Change the casing for BackingStoreError Signed-off-by: Martin Musale --- kiota_http/_exceptions.py | 2 +- kiota_http/httpx_request_adapter.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kiota_http/_exceptions.py b/kiota_http/_exceptions.py index 42f5816..576044d 100644 --- a/kiota_http/_exceptions.py +++ b/kiota_http/_exceptions.py @@ -5,7 +5,7 @@ class KiotaHTTPXError(Exception): """Base class for Kiota HTTP exceptions.""" -class BackingstoreError(KiotaHTTPXError): +class BackingStoreError(KiotaHTTPXError): """Raised for the backing store.""" diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index a6d6703..40a693f 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -26,7 +26,7 @@ from opentelemetry import trace from opentelemetry.semconv.trace import SpanAttributes -from kiota_http._exceptions import BackingstoreError, DeserializationError, RequestError +from kiota_http._exceptions import BackingStoreError, DeserializationError, RequestError from kiota_http.middleware.parameters_name_decoding_handler import ParametersNameDecodingHandler from ._version import VERSION @@ -371,7 +371,7 @@ def enable_backing_store(self, backing_store_factory: Optional[BackingStoreFacto ) ) if not any([self._serialization_writer_factory, self._parse_node_factory]): - raise BackingstoreError("Unable to enable backing store") + raise BackingStoreError("Unable to enable backing store") if backing_store_factory: BackingStoreFactorySingleton.__instance = backing_store_factory From b6be5927f3f7a43a19971e906fb4a7223b3e3d3c Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Mon, 11 Sep 2023 20:32:28 +0300 Subject: [PATCH 47/60] Update httpx and httpcore Signed-off-by: Martin Musale --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8ef48a7..7b399e0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -82,9 +82,9 @@ h2==4.1.0 hpack==4.0.0 -httpcore==0.17.3 +httpcore==0.18.0 -httpx[http2]==0.24.1 +httpx[http2]==0.25.0 hyperframe==6.0.1 From 5e6d90c971647ea4761906b1d3574f4f6c1d885a Mon Sep 17 00:00:00 2001 From: Martin Musale Date: Mon, 11 Sep 2023 20:35:18 +0300 Subject: [PATCH 48/60] Remove TODO and fix code smell Signed-off-by: Martin Musale --- kiota_http/httpx_request_adapter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 00da213..dc5ff62 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -413,7 +413,6 @@ async def throw_failed_responses( if response.is_success: return try: - # TODO: set status for the parent only? attribute_span.set_status(trace.StatusCode.ERROR) _throw_failed_resp_span = self._start_local_tracing_span( @@ -435,7 +434,6 @@ async def throw_failed_responses( ) # set this or ignore as description in set_status? _throw_failed_resp_span.set_attribute("status_message", "received_error_response") - # TODO: set status for just this span or the parent as well? _throw_failed_resp_span.set_status(trace.StatusCode.ERROR, str(exc)) attribute_span.record_exception(exc) raise exc From b1fa8f2d7a21719e4cb165a3df864f5215ab70ff Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:44:32 +0300 Subject: [PATCH 49/60] Fix merge conflict --- kiota_http/httpx_request_adapter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index ff85665..98f3389 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -490,15 +490,15 @@ async def throw_failed_responses( _throw_failed_resp_span.end() async def get_http_response_message( - self, request_info: RequestInformation, parent_span: trace.Span + self, + request_info: RequestInformation, + parent_span: trace.Span + claims: str = "" ) -> httpx.Response: _get_http_resp_span = self._start_local_tracing_span( "get_http_response_message", parent_span ) - async def get_http_response_message( - self, request_info: RequestInformation, claims: str = "" - ) -> httpx.Response: self.set_base_url_for_request_information(request_info) additional_authentication_context = None From c38cf76443916af96fe33baddf3017f3ae1b7678 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:50:12 +0300 Subject: [PATCH 50/60] Fix typo --- kiota_http/httpx_request_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 98f3389..c2f2158 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -492,7 +492,7 @@ async def throw_failed_responses( async def get_http_response_message( self, request_info: RequestInformation, - parent_span: trace.Span + parent_span: trace.Span, claims: str = "" ) -> httpx.Response: _get_http_resp_span = self._start_local_tracing_span( From f945d2dabac2d97ea775f97d8bda08c568fecb4a Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:10:38 +0300 Subject: [PATCH 51/60] Add tracing for CAE retries --- kiota_http/httpx_request_adapter.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index c2f2158..68b4c30 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -38,14 +38,7 @@ ResponseType = Union[str, int, float, bool, datetime, bytes] ModelType = TypeVar("ModelType", bound=Parsable) -RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" -ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" -ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" -DESERIALIZED_MODEL_NAME_KEY = "com.microsoft.kiota.response.type" -REQUEST_IS_NULL = RequestError("Request info cannot be null") - -tracer = trace.get_tracer(ObservabilityOptions.get_tracer_instrumentation_name(), VERSION) - +AUTHENTICATE_CHALLENGED_EVENT_KEY = "com.microsoft.kiota.authenticate_challenge_received" RESPONSE_HANDLER_EVENT_INVOKED_KEY = "response_handler_invoked" ERROR_MAPPING_FOUND_KEY = "com.microsoft.kiota.error.mapping_found" ERROR_BODY_FOUND_KEY = "com.microsoft.kiota.error.body_found" @@ -528,6 +521,7 @@ async def get_http_response_message( async def retry_cae_response_if_required( self, resp: httpx.Response, request_info: RequestInformation, claims: str ) -> httpx.Response: + parent_span = self.start_tracing_span(request_info, "retry_cae_response_if_required") if ( resp.status_code == 401 and not claims # previous claims exist. Means request has already been retried @@ -541,7 +535,9 @@ async def retry_cae_response_if_required( if not claims_match: raise ValueError("Unable to parse claims from response") response_claims = claims_match.group().split('="')[1] - return await self.get_http_response_message(request_info, response_claims) + parent_span.add_event(AUTHENTICATE_CHALLENGED_EVENT_KEY) + parent_span.set_attribute("http.retry_count", 1) + return await self.get_http_response_message(request_info, parent_span, response_claims) return resp return resp From ee257d3990d02714df2664822a0c971132895ffd Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:13:42 +0300 Subject: [PATCH 52/60] Code formatting --- kiota_http/httpx_request_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 68b4c30..c41364d 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -536,7 +536,7 @@ async def retry_cae_response_if_required( raise ValueError("Unable to parse claims from response") response_claims = claims_match.group().split('="')[1] parent_span.add_event(AUTHENTICATE_CHALLENGED_EVENT_KEY) - parent_span.set_attribute("http.retry_count", 1) + parent_span.set_attribute("http.retry_count", 1) return await self.get_http_response_message(request_info, parent_span, response_claims) return resp return resp From ea67a9146e1e7d6032db46606fe71b6811792efe Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:20:03 +0300 Subject: [PATCH 53/60] Fix code format --- kiota_http/httpx_request_adapter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index c41364d..06beb2e 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -536,8 +536,10 @@ async def retry_cae_response_if_required( raise ValueError("Unable to parse claims from response") response_claims = claims_match.group().split('="')[1] parent_span.add_event(AUTHENTICATE_CHALLENGED_EVENT_KEY) - parent_span.set_attribute("http.retry_count", 1) - return await self.get_http_response_message(request_info, parent_span, response_claims) + parent_span.set_attribute("http.retry_count", 1) + return await self.get_http_response_message( + request_info, parent_span, response_claims + ) return resp return resp From c72f530ccd3f11265d574d46106fdac0b988bcf3 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:24:36 +0300 Subject: [PATCH 54/60] Fix indentation --- kiota_http/httpx_request_adapter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index 06beb2e..cace624 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -536,10 +536,10 @@ async def retry_cae_response_if_required( raise ValueError("Unable to parse claims from response") response_claims = claims_match.group().split('="')[1] parent_span.add_event(AUTHENTICATE_CHALLENGED_EVENT_KEY) - parent_span.set_attribute("http.retry_count", 1) + parent_span.set_attribute("http.retry_count", 1) return await self.get_http_response_message( - request_info, parent_span, response_claims - ) + request_info, parent_span, response_claims + ) return resp return resp From d1e1f5b47fbac1add83010966b1c589abc18d62e Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:45:22 +0300 Subject: [PATCH 55/60] Add tracer to cae test --- tests/test_httpx_request_adapter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_httpx_request_adapter.py b/tests/test_httpx_request_adapter.py index 24c303d..dee2b8e 100644 --- a/tests/test_httpx_request_adapter.py +++ b/tests/test_httpx_request_adapter.py @@ -286,10 +286,12 @@ async def test_observability(request_adapter, request_info, mock_user_response, assert not trace.get_current_span().is_recording() @pytest.mark.asyncio -async def test_retries_on_cae_failure(request_adapter, request_info_mock, mock_cae_failure_response): +async def test_retries_on_cae_failure( + request_adapter, request_info_mock, mock_cae_failure_response, mock_otel_span +): request_adapter._http_client.send = AsyncMock(return_value=mock_cae_failure_response) request_adapter._authentication_provider.authenticate_request = AsyncMock() - resp = await request_adapter.get_http_response_message(request_info_mock) + resp = await request_adapter.get_http_response_message(request_info_mock, mock_otel_span) assert isinstance(resp, httpx.Response) calls = [ call(request_info_mock, None), From 2f6e49e0897ae7d112efb217b99fd40f8a2d0cc8 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:06:39 +0300 Subject: [PATCH 56/60] Bugfix additional authentication context --- kiota_http/httpx_request_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kiota_http/httpx_request_adapter.py b/kiota_http/httpx_request_adapter.py index cace624..4192b5b 100644 --- a/kiota_http/httpx_request_adapter.py +++ b/kiota_http/httpx_request_adapter.py @@ -494,9 +494,9 @@ async def get_http_response_message( self.set_base_url_for_request_information(request_info) - additional_authentication_context = None + additional_authentication_context = {} if claims: - additional_authentication_context = {self.CLAIMS_KEY: claims} + additional_authentication_context[self.CLAIMS_KEY] = claims await self._authentication_provider.authenticate_request( request_info, additional_authentication_context From ed71c3f945cee8f40a98061ce090292216e94954 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:14:18 +0300 Subject: [PATCH 57/60] Fix failing CAE test --- tests/test_httpx_request_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_httpx_request_adapter.py b/tests/test_httpx_request_adapter.py index dee2b8e..ef3ad34 100644 --- a/tests/test_httpx_request_adapter.py +++ b/tests/test_httpx_request_adapter.py @@ -294,7 +294,7 @@ async def test_retries_on_cae_failure( resp = await request_adapter.get_http_response_message(request_info_mock, mock_otel_span) assert isinstance(resp, httpx.Response) calls = [ - call(request_info_mock, None), + call(request_info_mock, {}), call(request_info_mock, {'claims': 'eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwNDEwNjY1MSJ9fX0'}) ] request_adapter._authentication_provider.authenticate_request.assert_has_awaits(calls) From 105c628b7814c5489d5a7a96e095c94d26e07475 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Wed, 13 Sep 2023 17:32:38 +0300 Subject: [PATCH 58/60] Fix bug in changes to redirect handler --- kiota_http/middleware/redirect_handler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kiota_http/middleware/redirect_handler.py b/kiota_http/middleware/redirect_handler.py index 3a1e915..8677770 100644 --- a/kiota_http/middleware/redirect_handler.py +++ b/kiota_http/middleware/redirect_handler.py @@ -85,6 +85,8 @@ async def send( request = new_request continue response.history = self.history + break + if not retryable: exc = RedirectError(f"Too many redirects. {response.history}") _redirect_span.record_exception(exc) _redirect_span.end() From ea8e0bca8d449054feb00d7e87a62f0414486cf0 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Wed, 13 Sep 2023 17:33:34 +0300 Subject: [PATCH 59/60] Bump abtractions version to fix failing tests --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 752b404..1c0fa8b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -86,7 +86,7 @@ httpx[http2]==0.25.0 hyperframe==6.0.1 -microsoft-kiota-abstractions==0.8.1 +microsoft-kiota-abstractions==0.8.2 sniffio==1.3.0 @@ -94,4 +94,4 @@ uritemplate==4.1.1 opentelemetry-api==1.19.0 -opentelemetry-sdk==1.19.0 \ No newline at end of file +opentelemetry-sdk==1.19.0 From 9b6229270fb79869b0b66a17ef47dcba52526791 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:49:51 +0300 Subject: [PATCH 60/60] Update abstractions version to 0.8.3 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c0fa8b..c22388f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -86,7 +86,7 @@ httpx[http2]==0.25.0 hyperframe==6.0.1 -microsoft-kiota-abstractions==0.8.2 +microsoft-kiota-abstractions==0.8.3 sniffio==1.3.0