Skip to content

Commit ca47287

Browse files
authored
Merge pull request #1129 from rksharma-owg/codex/graph-kiota-request-options
fix: honor Kiota request extensions in Graph transport
2 parents ec79992 + 156d947 commit ca47287

4 files changed

Lines changed: 164 additions & 9 deletions

File tree

‎pyproject.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ version = "1.5.1"
1111
authors = [{name = "Microsoft", email = "graphtooling+python@microsoft.com"}]
1212
description = "Core component of the Microsoft Graph Python SDK"
1313
dependencies = [
14-
"microsoft-kiota-abstractions >=1.11.6,<2.0.0",
15-
"microsoft-kiota-authentication-azure >=1.11.6,<2.0.0",
16-
"microsoft-kiota-http >=1.11.6,<2.0.0",
14+
"microsoft-kiota-abstractions >=1.13.0,<2.0.0",
15+
"microsoft-kiota-authentication-azure >=1.13.0,<2.0.0",
16+
"microsoft-kiota-http >=1.13.0,<2.0.0",
1717
"httpx[http2] >=0.23.0",
1818
]
1919
requires-python = ">=3.10"

‎requirements-dev.txt‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,13 @@ httpx[http2]==0.28.1
144144

145145
hyperframe==6.1.0 ; python_full_version >= '3.6.1'
146146

147-
microsoft-kiota-abstractions==1.11.6
147+
microsoft-kiota-abstractions==1.13.0
148148

149-
microsoft-kiota-authentication-azure==1.11.6
149+
microsoft-kiota-authentication-azure==1.13.0
150150

151-
microsoft-kiota-http==1.11.6
151+
microsoft-kiota-http==1.13.0
152152

153-
microsoft-kiota-serialization-json==1.11.6
153+
microsoft-kiota-serialization-json==1.13.0
154154

155155
multidict==6.7.1 ; python_version >= '3.7'
156156

‎src/msgraph_core/middleware/async_graph_transport.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from .._enums import FeatureUsageFlag
77
from .request_context import GraphRequestContext
88

9+
# Older supported Kiota releases do not export this request extension key.
10+
REQUEST_OPTIONS_KEY = 'kiota_request_options'
11+
912

1013
class AsyncGraphTransport(httpx.AsyncBaseTransport):
1114
"""A custom transport for requests to the Microsoft Graph API
@@ -16,7 +19,9 @@ def __init__(self, transport: httpx.AsyncBaseTransport, pipeline: MiddlewarePipe
1619
self.pipeline = pipeline
1720

1821
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
19-
if self.pipeline and hasattr(request, 'options'):
22+
if self.pipeline and (
23+
REQUEST_OPTIONS_KEY in request.extensions or hasattr(request, 'options')
24+
):
2025
self.set_request_context_and_feature_usage(request)
2126
response = await self.pipeline.send(request)
2227
return response
@@ -26,7 +31,11 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
2631

2732
def set_request_context_and_feature_usage(self, request: httpx.Request) -> httpx.Request:
2833

29-
request_options = request.options # type:ignore
34+
if REQUEST_OPTIONS_KEY in request.extensions:
35+
request_options = request.extensions[REQUEST_OPTIONS_KEY]
36+
else:
37+
request_options = request.options # type:ignore
38+
request.extensions[REQUEST_OPTIONS_KEY] = request_options
3039

3140
context = GraphRequestContext(request_options, request.headers)
3241
middleware = self.pipeline._first_middleware

‎tests/middleware/test_async_graph_transport.py‎

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1+
import asyncio
2+
3+
import httpx
14
import pytest
5+
from kiota_abstractions.authentication import AnonymousAuthenticationProvider
6+
from kiota_abstractions.method import Method
7+
from kiota_abstractions.request_information import RequestInformation
8+
from kiota_http.httpx_request_adapter import HttpxRequestAdapter
29
from kiota_http.kiota_client_factory import KiotaClientFactory
10+
from kiota_http.middleware.options import RedirectHandlerOption
311

412
from msgraph_core._enums import FeatureUsageFlag
13+
from msgraph_core.graph_client_factory import GraphClientFactory
514
from msgraph_core.middleware import AsyncGraphTransport, GraphRequestContext
15+
from msgraph_core.middleware.async_graph_transport import REQUEST_OPTIONS_KEY
616

717

818
def test_set_request_context_and_feature_usage(mock_request, mock_transport):
@@ -16,3 +26,139 @@ def test_set_request_context_and_feature_usage(mock_request, mock_transport):
1626
assert mock_request.context.feature_usage == hex(
1727
FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED
1828
)
29+
30+
31+
@pytest.mark.parametrize(
32+
'content_type', [
33+
'application/octet-stream',
34+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
35+
]
36+
)
37+
def test_binary_download_follows_redirect_with_kiota_request_extensions(content_type):
38+
calls = []
39+
contexts = []
40+
41+
def handle_request(request):
42+
calls.append(str(request.url))
43+
contexts.append(request.context)
44+
if request.url.host == 'graph.example':
45+
return httpx.Response(302, headers={'Location': 'https://download.example/file'})
46+
return httpx.Response(
47+
200, content=b'binary content', headers={'Content-Type': content_type}
48+
)
49+
50+
async def download():
51+
client = GraphClientFactory.create_with_default_middleware(
52+
client=httpx.AsyncClient(transport=httpx.MockTransport(handle_request))
53+
)
54+
try:
55+
adapter = HttpxRequestAdapter(AnonymousAuthenticationProvider(), http_client=client)
56+
request_info = RequestInformation()
57+
request_info.http_method = Method.GET
58+
request_info.url = 'https://graph.example/drive/item/content'
59+
return await adapter.send_primitive_async(request_info, 'bytes', {})
60+
finally:
61+
await client.aclose()
62+
63+
assert asyncio.run(download()) == b'binary content'
64+
assert calls == ['https://graph.example/drive/item/content', 'https://download.example/file']
65+
assert all(isinstance(context, GraphRequestContext) for context in contexts)
66+
67+
68+
def test_extension_options_take_precedence_over_legacy_attribute(mock_transport):
69+
middleware = KiotaClientFactory.get_default_middleware(None)
70+
pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, mock_transport)
71+
transport = AsyncGraphTransport(mock_transport, pipeline)
72+
request = httpx.Request('GET', 'https://example.org', extensions={REQUEST_OPTIONS_KEY: {}})
73+
request.options = {'legacy': True}
74+
75+
transport.set_request_context_and_feature_usage(request)
76+
77+
assert request.context.middleware_control == {}
78+
79+
80+
def test_request_without_options_bypasses_graph_pipeline():
81+
calls = []
82+
83+
def handle_request(request):
84+
calls.append(request)
85+
return httpx.Response(200, content=b'body')
86+
87+
async def send():
88+
underlying_transport = httpx.MockTransport(handle_request)
89+
middleware = KiotaClientFactory.get_default_middleware(None)
90+
pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, underlying_transport)
91+
transport = AsyncGraphTransport(underlying_transport, pipeline)
92+
return await transport.handle_async_request(httpx.Request('GET', 'https://example.org'))
93+
94+
assert asyncio.run(send()).status_code == 200
95+
assert len(calls) == 1
96+
assert not hasattr(calls[0], 'context')
97+
98+
99+
def test_extension_only_request_uses_graph_pipeline():
100+
requests = []
101+
102+
def handle_request(request):
103+
requests.append(request)
104+
return httpx.Response(200, content=b'body')
105+
106+
async def send():
107+
underlying_transport = httpx.MockTransport(handle_request)
108+
middleware = KiotaClientFactory.get_default_middleware(None)
109+
pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, underlying_transport)
110+
transport = AsyncGraphTransport(underlying_transport, pipeline)
111+
request = httpx.Request('GET', 'https://example.org', extensions={REQUEST_OPTIONS_KEY: {}})
112+
assert not hasattr(request, 'options')
113+
return await transport.handle_async_request(request)
114+
115+
assert asyncio.run(send()).content == b'body'
116+
assert len(requests) == 1
117+
assert isinstance(requests[0].context, GraphRequestContext)
118+
assert requests[0].context.middleware_control == {}
119+
assert requests[0].context.feature_usage == hex(
120+
FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED
121+
)
122+
123+
124+
@pytest.mark.parametrize(
125+
'legacy_redirect, extension_options, expected_status, expected_calls', [
126+
(False, None, 302, 1),
127+
(None, None, 200, 2),
128+
(True, False, 302, 1),
129+
(False, True, 200, 2),
130+
(False, {}, 200, 2),
131+
]
132+
)
133+
def test_request_redirect_options_are_honored(
134+
legacy_redirect, extension_options, expected_status, expected_calls
135+
):
136+
requests = []
137+
138+
def handle_request(request):
139+
requests.append(request)
140+
if request.url.path == '/start':
141+
return httpx.Response(302, headers={'Location': 'https://example.org/end'})
142+
return httpx.Response(200, content=b'body')
143+
144+
async def send():
145+
underlying_transport = httpx.MockTransport(handle_request)
146+
middleware = KiotaClientFactory.get_default_middleware(None)
147+
pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, underlying_transport)
148+
transport = AsyncGraphTransport(underlying_transport, pipeline)
149+
request = httpx.Request('GET', 'https://example.org/start')
150+
request.options = {}
151+
if legacy_redirect is not None:
152+
option = RedirectHandlerOption(should_redirect=legacy_redirect)
153+
request.options[option.get_key()] = option
154+
if extension_options is not None:
155+
options = extension_options
156+
if isinstance(options, bool):
157+
option = RedirectHandlerOption(should_redirect=options)
158+
options = {option.get_key(): option}
159+
request.extensions[REQUEST_OPTIONS_KEY] = options
160+
return await transport.handle_async_request(request)
161+
162+
assert asyncio.run(send()).status_code == expected_status
163+
assert len(requests) == expected_calls
164+
assert isinstance(requests[0].context, GraphRequestContext)

0 commit comments

Comments
 (0)