Skip to content

Commit 75a51f0

Browse files
authored
Merge pull request #99 from microsoftgraph/bug/enable-streaming
Fix bug on prepared request in graph client
2 parents e8a95c0 + 2ae58f6 commit 75a51f0

10 files changed

+116
-113
lines changed

.github/workflows/ci.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
run: |
2525
python -m pip install --upgrade pip
2626
pip install pipenv
27-
pipenv install --dev
27+
pipenv install --dev --skip-lock
2828
- name: Check code format
2929
run: |
3030
pipenv run yapf -dr .
@@ -37,3 +37,7 @@ jobs:
3737
- name: Test with pytest
3838
run: |
3939
pipenv run pytest
40+
env:
41+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
42+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
43+
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}

msgraph/core/_graph_client.py

+36-25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
# ------------------------------------
5+
import json
6+
57
from requests import Request, Session
68

79
from ._client_factory import HTTPClientFactory
@@ -25,23 +27,23 @@
2527
]
2628

2729

28-
def attach_context(func):
29-
"""Attaches a request context object to every graph request"""
30+
def collect_options(func):
31+
"""Collect middleware options into a middleware control dict and pass it as a header"""
3032
def wrapper(*args, **kwargs):
33+
3134
middleware_control = dict()
3235

3336
for option in supported_options:
3437
value = kwargs.pop(option, None)
3538
if value:
3639
middleware_control.update({option: value})
3740

38-
headers = kwargs.get('headers', {})
39-
request_context = RequestContext(middleware_control, headers)
40-
41-
request = func(*args, **kwargs)
42-
request.context = request_context
41+
if 'headers' in kwargs.keys():
42+
kwargs['headers'].update({'middleware_control': json.dumps(middleware_control)})
43+
else:
44+
kwargs['headers'] = {'middleware_control': json.dumps(middleware_control)}
4345

44-
return request
46+
return func(*args, **kwargs)
4547

4648
return wrapper
4749

@@ -81,16 +83,34 @@ def __init__(self, **kwargs):
8183
"""
8284
self.graph_session = self.get_graph_session(**kwargs)
8385

86+
@collect_options
8487
def get(self, url: str, **kwargs):
8588
r"""Sends a GET request. Returns :class:`Response` object.
8689
:param url: URL for the new :class:`Request` object.
8790
:param \*\*kwargs: Optional arguments that ``request`` takes.
8891
:rtype: requests.Response
8992
"""
90-
prepared_request = self.prepare_request('GET', self._graph_url(url), **kwargs)
91-
return self.graph_session.send(prepared_request)
93+
return self.graph_session.get(self._graph_url(url), **kwargs)
94+
95+
def options(self, url, **kwargs):
96+
r"""Sends a OPTIONS request. Returns :class:`Response` object.
97+
:param url: URL for the new :class:`Request` object.
98+
:param \*\*kwargs: Optional arguments that ``request`` takes.
99+
:rtype: requests.Response
100+
"""
101+
102+
return self.graph_session.options(self._graph_url(url), **kwargs)
92103

93-
def post(self, url, **kwargs):
104+
def head(self, url, **kwargs):
105+
r"""Sends a HEAD request. Returns :class:`Response` object.
106+
:param url: URL for the new :class:`Request` object.
107+
:param \*\*kwargs: Optional arguments that ``request`` takes.
108+
:rtype: requests.Response
109+
"""
110+
111+
return self.graph_session.head(self._graph_url(url), **kwargs)
112+
113+
def post(self, url, data=None, json=None, **kwargs):
94114
r"""Sends a POST request. Returns :class:`Response` object.
95115
:param url: URL for the new :class:`Request` object.
96116
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
@@ -99,8 +119,7 @@ def post(self, url, **kwargs):
99119
:param \*\*kwargs: Optional arguments that ``request`` takes.
100120
:rtype: requests.Response
101121
"""
102-
prepared_request = self.prepare_request('POST', self._graph_url(url), **kwargs)
103-
return self.graph_session.send(prepared_request)
122+
return self.graph_session.post(self._graph_url(url), data=data, json=json, **kwargs)
104123

105124
def put(self, url, data=None, **kwargs):
106125
r"""Sends a PUT request. Returns :class:`Response` object.
@@ -110,8 +129,8 @@ def put(self, url, data=None, **kwargs):
110129
:param \*\*kwargs: Optional arguments that ``request`` takes.
111130
:rtype: requests.Response
112131
"""
113-
prepared_request = self.prepare_request('PUT', self._graph_url(url), **kwargs)
114-
return self.graph_session.send(prepared_request)
132+
133+
return self.graph_session.put(self._graph_url(url), data=data, **kwargs)
115134

116135
def patch(self, url, data=None, **kwargs):
117136
r"""Sends a PATCH request. Returns :class:`Response` object.
@@ -121,17 +140,15 @@ def patch(self, url, data=None, **kwargs):
121140
:param \*\*kwargs: Optional arguments that ``request`` takes.
122141
:rtype: requests.Response
123142
"""
124-
prepared_request = self.prepare_request('PATCH', self._graph_url(url), **kwargs)
125-
return self.graph_session.send(prepared_request)
143+
return self.graph_session.patch(self._graph_url(url), data=data, **kwargs)
126144

127145
def delete(self, url, **kwargs):
128146
r"""Sends a DELETE request. Returns :class:`Response` object.
129147
:param url: URL for the new :class:`Request` object.
130148
:param \*\*kwargs: Optional arguments that ``request`` takes.
131149
:rtype: requests.Response
132150
"""
133-
prepared_request = self.prepare_request('DELETE', self._graph_url(url), **kwargs)
134-
return self.graph_session.send(prepared_request)
151+
return self.graph_session.delete(self._graph_url(url), **kwargs)
135152

136153
def _graph_url(self, url: str) -> str:
137154
"""Appends BASE_URL to user provided path
@@ -140,12 +157,6 @@ def _graph_url(self, url: str) -> str:
140157
"""
141158
return self.graph_session.base_url + url if (url[0] == '/') else url
142159

143-
@attach_context
144-
def prepare_request(self, method, url, **kwargs):
145-
req = Request(method, url, **kwargs)
146-
prepared = Session().prepare_request(req)
147-
return prepared
148-
149160
@staticmethod
150161
def get_graph_session(**kwargs):
151162
"""Method to always return a single instance of a HTTP Client"""

msgraph/core/middleware/middleware.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
# ------------------------------------
5+
import json
56
import ssl
67

78
from requests.adapters import HTTPAdapter
@@ -31,9 +32,13 @@ def add_middleware(self, middleware):
3132

3233
def send(self, request, **kwargs):
3334

34-
if not hasattr(request, 'context'):
35-
headers = request.headers
36-
request.context = RequestContext(dict(), headers)
35+
middleware_control_json = request.headers.pop('middleware_control', None)
36+
if middleware_control_json:
37+
middleware_control = json.loads(middleware_control_json)
38+
else:
39+
middleware_control = dict()
40+
41+
request.context = RequestContext(middleware_control, request.headers)
3742

3843
if self._middleware_present():
3944
return self._first_middleware.send(request, **kwargs)

tests/integration/test_graphclient.py

+28-35
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,20 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
import pytest
6+
from azure.identity import EnvironmentCredential
67
from requests import Session
78

89
from msgraph.core import APIVersion, GraphClient
910
from msgraph.core.middleware.authorization import AuthorizationHandler
1011

1112

12-
class _CustomTokenCredential:
13-
def get_token(self, scopes):
14-
return ['{token:https://graph.microsoft.com/}']
15-
16-
1713
def test_graph_client_with_default_middleware():
1814
"""
1915
Test that a graph client uses default middleware if none are provided
2016
"""
21-
credential = _CustomTokenCredential()
22-
client = GraphClient(credential=credential)
23-
response = client.get(
24-
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me'
25-
)
17+
# credential = _CustomTokenCredential()
18+
client = GraphClient(credential=EnvironmentCredential())
19+
response = client.get('https://graph.microsoft.com/v1.0/users')
2620
assert response.status_code == 200
2721

2822

@@ -32,52 +26,42 @@ def test_graph_client_with_user_provided_session():
3226
"""
3327

3428
session = Session()
35-
credential = _CustomTokenCredential()
36-
client = GraphClient(session=session, credential=credential)
37-
response = client.get(
38-
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me'
39-
)
29+
client = GraphClient(session=session, credential=EnvironmentCredential())
30+
response = client.get('https://graph.microsoft.com/v1.0/users', )
4031
assert response.status_code == 200
4132

4233

4334
def test_graph_client_with_custom_settings():
4435
"""
4536
Test that the graph client works with user provided configuration
4637
"""
47-
credential = _CustomTokenCredential()
38+
credential = EnvironmentCredential()
4839
client = GraphClient(api_version=APIVersion.beta, credential=credential)
49-
response = client.get(
50-
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me'
51-
)
40+
response = client.get('https://graph.microsoft.com/v1.0/users', )
5241
assert response.status_code == 200
5342

5443

5544
def test_graph_client_with_custom_middleware():
5645
"""
5746
Test client factory works with user provided middleware
5847
"""
59-
credential = _CustomTokenCredential()
48+
credential = EnvironmentCredential()
6049
middleware = [
6150
AuthorizationHandler(credential),
6251
]
6352
client = GraphClient(middleware=middleware)
64-
response = client.get(
65-
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me'
66-
)
53+
response = client.get('https://graph.microsoft.com/v1.0/users', )
6754
assert response.status_code == 200
6855

6956

7057
def test_graph_client_adds_context_to_request():
7158
"""
7259
Test the graph client adds a context object to a request
7360
"""
74-
credential = _CustomTokenCredential()
75-
scopes = ['User.Read.All']
61+
credential = EnvironmentCredential()
62+
scopes = ['https://graph.microsoft.com/.default']
7663
client = GraphClient(credential=credential)
77-
response = client.get(
78-
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me',
79-
scopes=scopes
80-
)
64+
response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes)
8165
assert response.status_code == 200
8266
assert hasattr(response.request, 'context')
8367

@@ -86,13 +70,22 @@ def test_graph_client_picks_options_from_kwargs():
8670
"""
8771
Test the graph client picks middleware options from kwargs and sets them in the context
8872
"""
89-
credential = _CustomTokenCredential()
90-
scopes = ['User.Read.All']
73+
credential = EnvironmentCredential()
74+
scopes = ['https://graph.microsoft.com/.default']
9175
client = GraphClient(credential=credential)
92-
response = client.get(
93-
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me',
94-
scopes=scopes
95-
)
76+
response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes)
9677
assert response.status_code == 200
9778
assert 'scopes' in response.request.context.middleware_control.keys()
9879
assert response.request.context.middleware_control['scopes'] == scopes
80+
81+
82+
def test_graph_client_allows_passing_optional_kwargs():
83+
"""
84+
Test the graph client allows passing optional kwargs native to the requests library
85+
such as stream, proxy and cert.
86+
"""
87+
credential = EnvironmentCredential()
88+
scopes = ['https://graph.microsoft.com/.default']
89+
client = GraphClient(credential=credential)
90+
response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes, stream=True)
91+
assert response.status_code == 200

0 commit comments

Comments
 (0)