Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

Commit

Permalink
Merge pull request #254 from microsoft/feature/support-additional-sta…
Browse files Browse the repository at this point in the history
…tus-codes

Feature/support additional status codes
  • Loading branch information
samwelkanda authored Nov 29, 2023
2 parents bbc1c56 + f3d3719 commit 91376f6
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 86 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2023-11-27

### Added

- Added support for additional status codes.

### Changed

## [1.0.0] - 2023-10-31

### Added
Expand Down
4 changes: 4 additions & 0 deletions kiota_http/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ class RequestError(KiotaHTTPXError):
"""Raised for request building errors."""


class ResponseError(KiotaHTTPXError):
"""Raised for response errors."""


class RedirectError(KiotaHTTPXError):
"""Raised when a redirect has errors."""
2 changes: 1 addition & 1 deletion kiota_http/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION: str = '1.0.0'
VERSION: str = '1.1.0'
59 changes: 39 additions & 20 deletions kiota_http/httpx_request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@
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,
ResponseError,
)
from kiota_http.middleware.parameters_name_decoding_handler import ParametersNameDecodingHandler

from ._version import VERSION
from .kiota_client_factory import KiotaClientFactory
from .middleware import ParametersNameDecodingHandler
from .middleware.options import ParametersNameDecodingHandlerOption, ResponseHandlerOption
from .observability_options import ObservabilityOptions

Expand Down Expand Up @@ -61,6 +67,7 @@ def __init__(
serialization_writer_factory:
SerializationWriterFactory = SerializationWriterFactoryRegistry(),
http_client: httpx.AsyncClient = KiotaClientFactory.create_with_default_middleware(),
base_url: str = "",
observability_options=ObservabilityOptions(),
) -> None:
if not authentication_provider:
Expand All @@ -78,7 +85,7 @@ def __init__(
observability_options = ObservabilityOptions()

self._http_client = http_client
self._base_url: str = ""
self._base_url: str = base_url
self.observability_options = observability_options

@property
Expand Down Expand Up @@ -127,8 +134,12 @@ def start_tracing_span(self, request_info: RequestInformation, method: str) -> t
Returns:
The parent span.
"""

uri_template = (request_info.url_template if request_info.url_template else "UNKNOWN")
decoded_uri_template = unquote(uri_template)
characters_to_decode_for_uri_template = ['$', '.', '-', '~']
decoded_uri_template = ParametersNameDecodingHandler().decode_uri_encoded_string(
uri_template, characters_to_decode_for_uri_template
)
parent_span_name = f"{method} - {decoded_uri_template}"

span = tracer.start_span(parent_span_name)
Expand Down Expand Up @@ -209,7 +220,6 @@ async def send_collection_async(
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)
if response_handler:
parent_span.add_event(RESPONSE_HANDLER_EVENT_INVOKED_KEY)
Expand All @@ -223,10 +233,12 @@ 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__)
_deserialized_span.end()
return result
if root_node:
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
return None
finally:
parent_span.end()

Expand Down Expand Up @@ -255,7 +267,6 @@ async def send_collection_of_primitive_async(
raise REQUEST_IS_NULL

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)
Expand All @@ -264,16 +275,17 @@ async def send_collection_of_primitive_async(
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
if root_node:
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
return None
finally:
parent_span.end()

Expand Down Expand Up @@ -315,7 +327,8 @@ async def send_primitive_async(
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 not root_node:
return None
if response_type == "str":
value = root_node.get_str_value()
if response_type == "int":
Expand All @@ -331,7 +344,7 @@ async def send_primitive_async(
_deserialized_span.end()
return value

exc = TypeError(f"Unable to deserialize type: {response_type!r}")
exc = TypeError(f"Error handling the response, unexpected type {response_type!r}")
parent_span.record_exception(exc)
_deserialized_span.end()
raise exc
Expand All @@ -356,7 +369,6 @@ async def send_no_response_content_async(
raise REQUEST_IS_NULL

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)
Expand Down Expand Up @@ -396,13 +408,13 @@ 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")
return None
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
return response.status_code == 204 or not bool(response.content)

async def throw_failed_responses(
self,
Expand Down Expand Up @@ -469,14 +481,19 @@ async def throw_failed_responses(
_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)

if not root_node:
return None
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)}",
(
"The server returned an unexpected status code and the error registered"
f" for this code failed to deserialize: {type(error)}"
),
response_status_code,
response_headers,
)
Expand Down Expand Up @@ -509,6 +526,8 @@ async def get_http_response_message(
request_info, _get_http_resp_span, parent_span
)
resp = await self._http_client.send(request)
if not resp:
raise ResponseError("Unable to get response from request")
parent_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, resp.status_code)
if http_version := resp.http_version:
parent_span.set_attribute(SpanAttributes.HTTP_FLAVOR, http_version)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
]
dynamic = ["version", "description"]
Expand Down
25 changes: 21 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from dataclasses import dataclass
from unittest.mock import AsyncMock

import httpx
import pytest
from unittest.mock import AsyncMock
from kiota_abstractions.api_error import APIError
from kiota_abstractions.method import Method
from kiota_abstractions.authentication import AnonymousAuthenticationProvider
from kiota_abstractions.method import Method
from kiota_abstractions.request_information import RequestInformation
from opentelemetry import trace

Expand Down Expand Up @@ -186,17 +186,35 @@ def mock_primitive_response_bytes(mocker):
)


@pytest.fixture
def mock_primitive_response_with_no_content(mocker):
return httpx.Response(
200,
headers={
"Content-Type":
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
)


@pytest.fixture
def mock_primitive_response_with_no_content_type_header(mocker):
return httpx.Response(200, content=b'Hello World')


@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")


@pytest.fixture
def mock_cae_failure_response(mocker):
auth_header = """Bearer authorization_uri="https://login.windows.net/common/oauth2/authorize",
Expand All @@ -205,8 +223,7 @@ def mock_cae_failure_response(mocker):
claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwNDEwNjY1MSJ9fX0="
"""
return httpx.Response(
401,
headers={
401, headers={
"Content-Type": "application/json",
"WWW-Authenticate": auth_header
}
Expand Down
Loading

0 comments on commit 91376f6

Please sign in to comment.