Skip to content

Commit 01c1311

Browse files
authored
Merge pull request #83 from microsoftgraph/feat/request-context
Feat/request context
2 parents a4ff042 + 644bfe1 commit 01c1311

20 files changed

+422
-335
lines changed

Pipfile

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ name = "pypi"
55

66
[packages] # Packages required to run the application
77
requests = "==2.23.0"
8-
pre-commit = "==2.10.1"
8+
azure-identity = "==1.6.0"
99

1010
[dev-packages] # Packages required to develop the application
1111
coverage = "==5.0.3"
1212
responses = "==0.10.12"
1313
flit = "==2.2.0"
14-
azure-identity = "==1.5.0"
1514
isort = "==5.7.0"
1615
yapf = "==0.30.0"
1716
mypy = "==0.800"
1817
pylint = "==2.7.4"
19-
pytest = "*"
18+
pytest = "==6.2.4"
19+
pre-commit = "==2.13.0"

Pipfile.lock

+229-225
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dev_requirements.txt

-5
This file was deleted.

docs/design/request_context.puml

Whitespace-only changes.

msgraphcore/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
from .constants import SDK_VERSION
1+
from msgraphcore.client_factory import HTTPClientFactory
2+
from msgraphcore.constants import SDK_VERSION
3+
from msgraphcore.graph_client import GraphClient
24

35
__version__ = SDK_VERSION

msgraphcore/enums.py

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ class APIVersion(str, Enum):
99
v1 = 'v1.0'
1010

1111

12+
class FeatureUsageFlag(int, Enum):
13+
"""Enumerated list of values used to flag usage of specific middleware"""
14+
15+
NONE = 0
16+
REDIRECT_HANDLER_ENABLED = 1
17+
RETRY_HANDLER_ENABLED = 2
18+
AUTH_HANDLER_ENABLED = 4
19+
DEFAULT_HTTP_PROVIDER_ENABLED = 8
20+
LOGGING_HANDLER_ENABLED = 16
21+
22+
1223
class NationalClouds(str, Enum):
1324
"""Enumerated list of supported sovereign clouds"""
1425

msgraphcore/graph_client.py

+44-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
from typing import List, Optional
22

3-
from requests import Session
3+
from requests import Request, Session
44

55
from msgraphcore.client_factory import HTTPClientFactory
66
from msgraphcore.middleware.abc_token_credential import TokenCredential
77
from msgraphcore.middleware.middleware import BaseMiddleware
8-
from msgraphcore.middleware.options.middleware_control import middleware_control
8+
from msgraphcore.middleware.request_context import RequestContext
9+
10+
supported_options = ['scopes', 'custom_option']
11+
12+
13+
def attach_context(func):
14+
def wrapper(*args, **kwargs):
15+
middleware_control = dict()
16+
17+
for option in supported_options:
18+
value = kwargs.pop(option, None)
19+
if value:
20+
middleware_control.update({option: value})
21+
22+
headers = kwargs.get('headers', {})
23+
request_context = RequestContext(middleware_control, headers)
24+
25+
request = func(*args, **kwargs)
26+
request.context = request_context
27+
28+
return request
29+
30+
return wrapper
931

1032

1133
class GraphClient:
@@ -43,17 +65,16 @@ def __init__(self, **kwargs):
4365
"""
4466
self.graph_session = self.get_graph_session(**kwargs)
4567

46-
@middleware_control.get_middleware_options
4768
def get(self, url: str, **kwargs):
4869
r"""Sends a GET request. Returns :class:`Response` object.
4970
:param url: URL for the new :class:`Request` object.
5071
:param \*\*kwargs: Optional arguments that ``request`` takes.
5172
:rtype: requests.Response
5273
"""
53-
return self.graph_session.get(self._graph_url(url), **kwargs)
74+
prepared_request = self.prepare_request('GET', self._graph_url(url), **kwargs)
75+
return self.graph_session.send(prepared_request)
5476

55-
@middleware_control.get_middleware_options
56-
def post(self, url, data=None, json=None, **kwargs):
77+
def post(self, url, **kwargs):
5778
r"""Sends a POST request. Returns :class:`Response` object.
5879
:param url: URL for the new :class:`Request` object.
5980
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
@@ -62,9 +83,9 @@ def post(self, url, data=None, json=None, **kwargs):
6283
:param \*\*kwargs: Optional arguments that ``request`` takes.
6384
:rtype: requests.Response
6485
"""
65-
return self.graph_session.post(self._graph_url(url), data, json, **kwargs)
86+
prepared_request = self.prepare_request('POST', self._graph_url(url), **kwargs)
87+
return self.graph_session.send(prepared_request)
6688

67-
@middleware_control.get_middleware_options
6889
def put(self, url, data=None, **kwargs):
6990
r"""Sends a PUT request. Returns :class:`Response` object.
7091
:param url: URL for the new :class:`Request` object.
@@ -73,9 +94,9 @@ def put(self, url, data=None, **kwargs):
7394
:param \*\*kwargs: Optional arguments that ``request`` takes.
7495
:rtype: requests.Response
7596
"""
76-
return self.graph_session.put(self._graph_url(url), data, **kwargs)
97+
prepared_request = self.prepare_request('PUT', self._graph_url(url), **kwargs)
98+
return self.graph_session.send(prepared_request)
7799

78-
@middleware_control.get_middleware_options
79100
def patch(self, url, data=None, **kwargs):
80101
r"""Sends a PATCH request. Returns :class:`Response` object.
81102
:param url: URL for the new :class:`Request` object.
@@ -84,16 +105,17 @@ def patch(self, url, data=None, **kwargs):
84105
:param \*\*kwargs: Optional arguments that ``request`` takes.
85106
:rtype: requests.Response
86107
"""
87-
return self.graph_session.patch(self._graph_url(url), data, **kwargs)
108+
prepared_request = self.prepare_request('PATCH', self._graph_url(url), **kwargs)
109+
return self.graph_session.send(prepared_request)
88110

89-
@middleware_control.get_middleware_options
90111
def delete(self, url, **kwargs):
91112
r"""Sends a DELETE request. Returns :class:`Response` object.
92113
:param url: URL for the new :class:`Request` object.
93114
:param \*\*kwargs: Optional arguments that ``request`` takes.
94115
:rtype: requests.Response
95116
"""
96-
return self.graph_session.delete(self._graph_url(url), **kwargs)
117+
prepared_request = self.prepare_request('DELETE', self._graph_url(url), **kwargs)
118+
return self.graph_session.send(prepared_request)
97119

98120
def _graph_url(self, url: str) -> str:
99121
"""Appends BASE_URL to user provided path
@@ -102,12 +124,18 @@ def _graph_url(self, url: str) -> str:
102124
"""
103125
return self.graph_session.base_url + url if (url[0] == '/') else url
104126

127+
@attach_context
128+
def prepare_request(self, method, url, **kwargs):
129+
req = Request(method, url, **kwargs)
130+
prepared = Session().prepare_request(req)
131+
return prepared
132+
105133
@staticmethod
106134
def get_graph_session(**kwargs):
107135
"""Method to always return a single instance of a HTTP Client"""
108136

109-
credential = kwargs.get('credential')
110-
middleware = kwargs.get('middleware')
137+
credential = kwargs.pop('credential', None)
138+
middleware = kwargs.pop('middleware', None)
111139

112140
if credential and middleware:
113141
raise ValueError(
@@ -117,5 +145,5 @@ def get_graph_session(**kwargs):
117145
raise ValueError("Invalid parameters!. Missing TokenCredential or middleware")
118146

119147
if credential:
120-
return HTTPClientFactory(**kwargs).create_with_default_middleware(credential)
148+
return HTTPClientFactory(**kwargs).create_with_default_middleware(credential, **kwargs)
121149
return HTTPClientFactory(**kwargs).create_with_custom_middleware(middleware)
+10-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from msgraphcore.constants import AUTH_MIDDLEWARE_OPTIONS
1+
from msgraphcore.enums import FeatureUsageFlag
22
from msgraphcore.middleware.abc_token_credential import TokenCredential
33
from msgraphcore.middleware.middleware import BaseMiddleware
4-
from msgraphcore.middleware.options.middleware_control import middleware_control
54

65

76
class AuthorizationHandler(BaseMiddleware):
@@ -12,7 +11,11 @@ def __init__(self, credential: TokenCredential, **kwargs):
1211
self.retry_count = 0
1312

1413
def send(self, request, **kwargs):
15-
request.headers.update({'Authorization': 'Bearer {}'.format(self._get_access_token())})
14+
context = request.context
15+
request.headers.update(
16+
{'Authorization': 'Bearer {}'.format(self._get_access_token(context))}
17+
)
18+
context.set_feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED
1619
response = super().send(request, **kwargs)
1720

1821
# Token might have expired just before transmission, retry the request one more time
@@ -21,13 +24,9 @@ def send(self, request, **kwargs):
2124
return self.send(request, **kwargs)
2225
return response
2326

24-
def _get_access_token(self):
25-
return self.credential.get_token(*self.get_scopes())[0]
27+
def _get_access_token(self, context):
28+
return self.credential.get_token(*self.get_scopes(context))[0]
2629

27-
def get_scopes(self):
30+
def get_scopes(self, context):
2831
# Checks if there are any options for this middleware
29-
auth_options_present = middleware_control.get(AUTH_MIDDLEWARE_OPTIONS)
30-
# If there is, get the scopes from the options
31-
if auth_options_present:
32-
return auth_options_present.scopes
33-
return self.scopes
32+
return context.middleware_control.get('scopes', self.scopes)

msgraphcore/middleware/middleware.py

+8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import ssl
2+
import uuid
23

34
from requests.adapters import HTTPAdapter
45
from urllib3 import PoolManager
56

7+
from msgraphcore.middleware.request_context import RequestContext
8+
69

710
class MiddlewarePipeline(HTTPAdapter):
811
"""MiddlewarePipeline, entry point of middleware
@@ -22,6 +25,11 @@ def add_middleware(self, middleware):
2225
self._middleware = middleware
2326

2427
def send(self, request, **kwargs):
28+
29+
if not hasattr(request, 'context'):
30+
headers = request.headers
31+
request.context = RequestContext(dict(), headers)
32+
2533
if self._middleware_present():
2634
return self._middleware.send(request, **kwargs)
2735
# No middleware in pipeline, call superclass' send

msgraphcore/middleware/options/auth_middleware_options.py

-3
This file was deleted.

msgraphcore/middleware/options/middleware_control.py

-34
This file was deleted.
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import uuid
2+
3+
from msgraphcore.enums import FeatureUsageFlag
4+
5+
6+
class RequestContext:
7+
def __init__(self, middleware_control, headers):
8+
self.middleware_control = middleware_control
9+
self.client_request_id = headers.get('client-request-id', str(uuid.uuid4()))
10+
self._feature_usage = FeatureUsageFlag.NONE
11+
12+
@property
13+
def feature_usage(self):
14+
return hex(self._feature_usage)
15+
16+
@feature_usage.setter
17+
def set_feature_usage(self, flag: FeatureUsageFlag):
18+
self._feature_usage = self._feature_usage | flag

requirements.txt

-1
This file was deleted.

samples/samples.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
from azure.identity import InteractiveBrowserCredential
55

6-
from msgraphcore import GraphSession
6+
from msgraphcore.graph_client import GraphClient
77

88
scopes = ['user.read']
9-
browser_credential = InteractiveBrowserCredential(client_id='ENTER_YOUR_CLIENT_ID')
10-
graph_session = GraphSession(browser_credential, scopes)
9+
browser_credential = InteractiveBrowserCredential(client_id='YOUR_CLIENT_ID')
10+
graph_session = GraphClient(credential=browser_credential)
1111

1212

1313
def post_sample():

tests/integration/test_graph_client.py renamed to tests/integration/test_graphclient.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test_graph_client_with_custom_settings():
4949
assert response.status_code == 200
5050

5151

52-
def test_client_factory_with_custom_middleware():
52+
def test_graph_client_with_custom_middleware():
5353
"""
5454
Test client factory works with user provided middleware
5555
"""
@@ -62,3 +62,34 @@ def test_client_factory_with_custom_middleware():
6262
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me'
6363
)
6464
assert response.status_code == 200
65+
66+
67+
def test_graph_client_adds_context_to_request():
68+
"""
69+
Test the graph client adds a context object to a request
70+
"""
71+
credential = _CustomTokenCredential()
72+
scopes = ['User.Read.All']
73+
client = GraphClient(credential=credential)
74+
response = client.get(
75+
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me',
76+
scopes=scopes
77+
)
78+
assert response.status_code == 200
79+
assert hasattr(response.request, 'context')
80+
81+
82+
def test_graph_client_picks_options_from_kwargs():
83+
"""
84+
Test the graph client picks middleware options from kwargs and sets them in the context
85+
"""
86+
credential = _CustomTokenCredential()
87+
scopes = ['User.Read.All']
88+
client = GraphClient(credential=credential)
89+
response = client.get(
90+
'https://proxy.apisandbox.msdn.microsoft.com/svc?url=https://graph.microsoft.com/v1.0/me',
91+
scopes=scopes
92+
)
93+
assert response.status_code == 200
94+
assert 'scopes' in response.request.context.middleware_control.keys()
95+
assert response.request.context.middleware_control['scopes'] == scopes

0 commit comments

Comments
 (0)