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

Commit cc92860

Browse files
authored
Merge pull request #296 from microsoft/feature/XXX-error-mapping
Feature/xxx error mapping
2 parents 52d279a + 07d118e commit cc92860

File tree

6 files changed

+106
-53
lines changed

6 files changed

+106
-53
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.3.0] - 2024-02-08
9+
10+
### Added
11+
12+
- Added support for `XXX` status code error mapping in RequestAdapter.[#280](https://github.com/microsoft/kiota-http-python/issues/280)
13+
14+
### Changed
15+
816
## [1.2.1] - 2024-01-22
917

1018
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Microsoft Kiota HTTP library
22
[![PyPI version](https://badge.fury.io/py/microsoft-kiota-http.svg)](https://badge.fury.io/py/microsoft-kiota-http)
3-
[![CI Actions Status](https://github.com/microsoft/kiota-http-python/actions/workflows/build_publish.yml/badge.svg?branch=main)](https://github.com/microsoft/kiota-http-python/actions)
3+
[![CI Actions Status](https://github.com/microsoft/kiota-http-python/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/microsoft/kiota-http-python/actions)
44
[![Downloads](https://pepy.tech/badge/microsoft-kiota-http)](https://pepy.tech/project/microsoft-kiota-http)
55

66
The Microsoft Kiota HTTP Library is a python HTTP implementation with HTTPX library.

kiota_http/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION: str = '1.2.1'
1+
VERSION: str = '1.3.0'

kiota_http/httpx_request_adapter.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,9 @@ async def throw_failed_responses(
451451
attribute_span.record_exception(exc)
452452
raise exc
453453

454-
if (response_status_code_str not in error_map) and (
455-
(400 <= response_status_code < 500 and "4XX" not in error_map) or
456-
(500 <= response_status_code < 600 and "5XX" not in error_map)
454+
if (
455+
response_status_code_str not in error_map
456+
and self._error_class_not_in_error_mapping(error_map, response_status_code)
457457
):
458458
exc = APIError(
459459
"The server returned an unexpected status code and no error class is registered"
@@ -466,12 +466,14 @@ async def throw_failed_responses(
466466
_throw_failed_resp_span.set_attribute("status_message", "received_error_response")
467467

468468
error_class = None
469-
if response_status_code_str in error_map:
469+
if response_status_code_str in error_map: # Error Code 400 - <= 599
470470
error_class = error_map[response_status_code_str]
471-
elif 400 <= response_status_code < 500:
471+
elif 400 <= response_status_code < 500 and "4XX" in error_map: # Error code 4XX
472472
error_class = error_map["4XX"]
473-
elif 500 <= response_status_code < 600:
473+
elif 500 <= response_status_code < 600 and "5XX" in error_map: # Error code 5XX
474474
error_class = error_map["5XX"]
475+
elif "XXX" in error_map: # Blanket case
476+
error_class = error_map["XXX"]
475477

476478
root_node = await self.get_root_parse_node(
477479
response, _throw_failed_resp_span, _throw_failed_resp_span
@@ -635,3 +637,22 @@ async def convert_to_native_async(self, request_info: RequestInformation) -> htt
635637
return request
636638
finally:
637639
parent_span.end()
640+
641+
def _error_class_not_in_error_mapping(
642+
self, error_map: Dict[str, ParsableFactory], status_code: int
643+
) -> bool:
644+
"""Helper function to check if the error class corresponding to a response status code
645+
is not in the error mapping.
646+
647+
Args:
648+
error_map (Dict[str, ParsableFactory]): The error mapping.
649+
status_code (int): The response status code.
650+
651+
Returns:
652+
bool: True if the error class is not in the error mapping, False otherwise.
653+
"""
654+
655+
return (
656+
(400 <= status_code < 500 and "4XX" not in error_map) or
657+
(500 <= status_code < 600 and "5XX" not in error_map)
658+
) and ("XXX" not in error_map)

tests/conftest.py

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
from .helpers import MockTransport, MockErrorObject, MockResponseObject, OfficeLocation
1515

16+
@pytest.fixture
17+
def sample_headers():
18+
return {"Content-Type": "application/json"}
1619

1720
@pytest.fixture
1821
def auth_provider():
@@ -49,54 +52,42 @@ def mock_error_object():
4952

5053

5154
@pytest.fixture
52-
def mock_error_map():
55+
def mock_error_500_map():
5356
return {
5457
"500": Exception("Internal Server Error"),
5558
}
5659

57-
5860
@pytest.fixture
59-
def mock_apierror_map():
61+
def mock_apierror_map(sample_headers):
6062
return {
61-
"500":
62-
APIError(
63-
"Custom Internal Server Error", {
64-
'cache-control': 'private',
65-
'transfer-encoding': 'chunked',
66-
'content-type': 'application/json'
67-
}, 500
68-
)
63+
"400": APIError("Resource not found", 400, sample_headers),
64+
"500": APIError("Custom Internal Server Error", 500, sample_headers)
6965
}
7066

71-
7267
@pytest.fixture
73-
def mock_request_adapter():
74-
resp = httpx.Response(
75-
json={'error': 'not found'}, status_code=404, headers={"Content-Type": "application/json"}
76-
)
68+
def mock_apierror_XXX_map(sample_headers):
69+
return {"XXX": APIError("OdataError",400, sample_headers)}
70+
71+
@pytest.fixture
72+
def mock_request_adapter(sample_headers):
73+
resp = httpx.Response(json={'error': 'not found'}, status_code=404, headers=sample_headers)
7774
mock_request_adapter = AsyncMock
7875
mock_request_adapter.get_http_response_message = AsyncMock(return_value=resp)
7976

80-
8177
@pytest.fixture
82-
def simple_error_response():
83-
return httpx.Response(
84-
json={'error': 'not found'}, status_code=404, headers={"Content-Type": "application/json"}
85-
)
86-
78+
def simple_error_response(sample_headers):
79+
return httpx.Response(json={'error': 'not found'}, status_code=404, headers=sample_headers)
8780

8881
@pytest.fixture
89-
def simple_success_response():
90-
return httpx.Response(
91-
json={'message': 'Success!'}, status_code=200, headers={"Content-Type": "application/json"}
92-
)
82+
def simple_success_response(sample_headers):
83+
return httpx.Response(json={'message': 'Success!'}, status_code=200, headers=sample_headers)
9384

9485

9586
@pytest.fixture
96-
def mock_user_response(mocker):
87+
def mock_user_response(mocker, sample_headers):
9788
return httpx.Response(
9889
200,
99-
headers={"Content-Type": "application/json"},
90+
headers=sample_headers,
10091
json={
10192
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
10293
"businessPhones": ["+1 205 555 0108"],
@@ -161,25 +152,25 @@ def mock_users_response(mocker):
161152

162153

163154
@pytest.fixture
164-
def mock_primitive_collection_response(mocker):
155+
def mock_primitive_collection_response(sample_headers):
165156
return httpx.Response(
166-
200, json=[12.1, 12.2, 12.3, 12.4, 12.5], headers={"Content-Type": "application/json"}
157+
200, json=[12.1, 12.2, 12.3, 12.4, 12.5], headers=sample_headers
167158
)
168159

169160

170161
@pytest.fixture
171-
def mock_primitive(mocker):
162+
def mock_primitive():
172163
resp = MockResponseObject()
173164
return resp
174165

175166

176167
@pytest.fixture
177-
def mock_primitive_response(mocker):
178-
return httpx.Response(200, json=22.3, headers={"Content-Type": "application/json"})
168+
def mock_primitive_response(sample_headers):
169+
return httpx.Response(200, json=22.3, headers=sample_headers)
179170

180171

181172
@pytest.fixture
182-
def mock_primitive_response_bytes(mocker):
173+
def mock_primitive_response_bytes():
183174
return httpx.Response(
184175
200,
185176
content=b'Hello World',
@@ -191,7 +182,7 @@ def mock_primitive_response_bytes(mocker):
191182

192183

193184
@pytest.fixture
194-
def mock_primitive_response_with_no_content(mocker):
185+
def mock_primitive_response_with_no_content():
195186
return httpx.Response(
196187
200,
197188
headers={
@@ -202,13 +193,13 @@ def mock_primitive_response_with_no_content(mocker):
202193

203194

204195
@pytest.fixture
205-
def mock_primitive_response_with_no_content_type_header(mocker):
196+
def mock_primitive_response_with_no_content_type_header():
206197
return httpx.Response(200, content=b'Hello World')
207198

208199

209200
@pytest.fixture
210-
def mock_no_content_response(mocker):
211-
return httpx.Response(204, json="Radom JSON", headers={"Content-Type": "application/json"})
201+
def mock_no_content_response(sample_headers):
202+
return httpx.Response(204, json="Radom JSON", headers=sample_headers)
212203

213204

214205
tracer = trace.get_tracer(__name__)
@@ -220,7 +211,7 @@ def mock_otel_span():
220211

221212

222213
@pytest.fixture
223-
def mock_cae_failure_response(mocker):
214+
def mock_cae_failure_response():
224215
auth_header = """Bearer authorization_uri="https://login.windows.net/common/oauth2/authorize",
225216
client_id="00000003-0000-0000-c000-000000000000",
226217
error="insufficient_claims",

tests/test_httpx_request_adapter.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
from .helpers import MockResponseObject
1919

20+
APPLICATION_JSON = "application/json"
2021
BASE_URL = "https://graph.microsoft.com"
2122

2223

24+
2325
def test_create_request_adapter(auth_provider):
2426
request_adapter = HttpxRequestAdapter(auth_provider)
2527
assert request_adapter._authentication_provider is auth_provider
@@ -53,7 +55,7 @@ def test_get_serialization_writer_factory(request_adapter):
5355

5456
def test_get_response_content_type(request_adapter, simple_success_response):
5557
content_type = request_adapter.get_response_content_type(simple_success_response)
56-
assert content_type == "application/json"
58+
assert content_type == APPLICATION_JSON
5759

5860

5961
def test_set_base_url_for_request_information(request_adapter, request_info):
@@ -146,7 +148,7 @@ async def test_throw_failed_responses_null_error_map(
146148

147149
@pytest.mark.asyncio
148150
async def test_throw_failed_responses_no_error_class(
149-
request_adapter, simple_error_response, mock_error_map, mock_otel_span
151+
request_adapter, simple_error_response, mock_error_500_map, mock_otel_span
150152
):
151153
assert simple_error_response.text == '{"error": "not found"}'
152154
assert simple_error_response.status_code == 404
@@ -156,7 +158,7 @@ async def test_throw_failed_responses_no_error_class(
156158
with pytest.raises(APIError) as e:
157159
span = mock_otel_span
158160
await request_adapter.throw_failed_responses(
159-
simple_error_response, mock_error_map, span, span
161+
simple_error_response, mock_error_500_map, span, span
160162
)
161163
assert (
162164
str(e.value.message) == "The server returned an unexpected status code and"
@@ -167,7 +169,7 @@ async def test_throw_failed_responses_no_error_class(
167169

168170
@pytest.mark.asyncio
169171
async def test_throw_failed_responses_not_apierror(
170-
request_adapter, mock_error_map, mock_error_object, mock_otel_span
172+
request_adapter, mock_error_500_map, mock_error_object, mock_otel_span
171173
):
172174
request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object)
173175
resp = httpx.Response(status_code=500, headers={"Content-Type": "application/json"})
@@ -177,15 +179,30 @@ async def test_throw_failed_responses_not_apierror(
177179

178180
with pytest.raises(Exception) as e:
179181
span = mock_otel_span
180-
await request_adapter.throw_failed_responses(resp, mock_error_map, span, span)
182+
await request_adapter.throw_failed_responses(resp, mock_error_500_map, span, span)
181183
assert ("The server returned an unexpected status code and the error registered"
182184
" for this code failed to deserialize") in str(
183185
e.value.message
184186
)
185187

186188

187189
@pytest.mark.asyncio
188-
async def test_throw_failed_responses(
190+
async def test_throw_failed_responses_4XX(
191+
request_adapter, mock_apierror_map, mock_error_object, mock_otel_span
192+
):
193+
request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object)
194+
resp = httpx.Response(status_code=400, headers={"Content-Type": "application/json"})
195+
assert resp.status_code == 400
196+
content_type = request_adapter.get_response_content_type(resp)
197+
assert content_type == "application/json"
198+
199+
with pytest.raises(APIError) as e:
200+
span = mock_otel_span
201+
await request_adapter.throw_failed_responses(resp, mock_apierror_map, span, span)
202+
assert str(e.value.message) == "Resource not found"
203+
204+
@pytest.mark.asyncio
205+
async def test_throw_failed_responses_5XX(
189206
request_adapter, mock_apierror_map, mock_error_object, mock_otel_span
190207
):
191208
request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object)
@@ -198,6 +215,22 @@ async def test_throw_failed_responses(
198215
span = mock_otel_span
199216
await request_adapter.throw_failed_responses(resp, mock_apierror_map, span, span)
200217
assert str(e.value.message) == "Custom Internal Server Error"
218+
219+
@pytest.mark.asyncio
220+
async def test_throw_failed_responses_XXX(
221+
request_adapter, mock_apierror_XXX_map, mock_error_object, mock_otel_span
222+
):
223+
request_adapter.get_root_parse_node = AsyncMock(return_value=mock_error_object)
224+
resp = httpx.Response(status_code=500, headers={"Content-Type": "application/json"})
225+
assert resp.status_code == 500
226+
content_type = request_adapter.get_response_content_type(resp)
227+
assert content_type == "application/json"
228+
229+
with pytest.raises(APIError) as e:
230+
span = mock_otel_span
231+
await request_adapter.throw_failed_responses(resp, mock_apierror_XXX_map, span, span)
232+
assert str(e.value.message) == "OdataError"
233+
201234

202235

203236
@pytest.mark.asyncio

0 commit comments

Comments
 (0)