Skip to content

Commit 14fe407

Browse files
authored
Merge pull request #41 from microsoftgraph/fix/post-requests
Fix/post requests
2 parents a91a347 + 4aa3c84 commit 14fe407

File tree

8 files changed

+174
-65
lines changed

8 files changed

+174
-65
lines changed

dev_requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
coverage==5.0.3
22
pylint==2.4.4
33
responses==0.10.12
4-
flit==2.2.0
4+
flit==2.2.0
5+
azure-identity==1.3.1

msgraphcore/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
BASE_URL = 'https://graph.microsoft.com/v1.0'
77
SDK_VERSION = '0.0.1-0'
88

9-
# MiddlewareOptions
9+
# Used as the key for AuthMiddlewareOption in MiddlewareControl
1010
AUTH_MIDDLEWARE_OPTIONS = 'AUTH_MIDDLEWARE_OPTIONS'

msgraphcore/graph_session.py

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
"""
22
Graph Session
33
"""
4-
from requests import Session, Request, Response
4+
5+
from requests import Session
56

67
from msgraphcore.constants import BASE_URL, SDK_VERSION
78
from msgraphcore.middleware._middleware import MiddlewarePipeline, BaseMiddleware
89
from msgraphcore.middleware._base_auth import AuthProviderBase
910
from msgraphcore.middleware.authorization import AuthorizationHandler
11+
from msgraphcore.middleware.options.middleware_control import middleware_control
1012

1113

1214
class GraphSession(Session):
13-
"""
14-
Extends session object with graph functionality
15+
"""Extends Session with Graph functionality
16+
17+
Extends Session by adding support for middleware options and middleware pipeline
18+
19+
1520
"""
1621
def __init__(self, auth_provider: AuthProviderBase, middleware: list = []):
1722
super().__init__()
@@ -20,47 +25,84 @@ def __init__(self, auth_provider: AuthProviderBase, middleware: list = []):
2025

2126
auth_handler = AuthorizationHandler(auth_provider)
2227

28+
# The authorization handler should be the first middleware in the pipeline.
2329
middleware.insert(0, auth_handler)
2430
self._register(middleware)
2531

26-
def get(self, url: str, **kwargs) -> Response:
27-
return self._prepare_and_send_request('GET', url, **kwargs)
28-
29-
def post(self, url: str, **kwargs) -> Response:
30-
return self._prepare_and_send_request('POST', url, **kwargs)
31-
32-
def put(self, url: str, **kwargs) -> Response:
33-
return self._prepare_and_send_request('PUT', url, **kwargs)
34-
35-
def patch(self, url: str, **kwargs) -> Response:
36-
return self._prepare_and_send_request('PATCH', url, **kwargs)
37-
38-
def delete(self, url: str, **kwargs) -> Response:
39-
return self._prepare_and_send_request('DELETE', url, **kwargs)
40-
41-
def _get_url(self, url: str) -> Response:
32+
@middleware_control.get_middleware_options
33+
def get(self, url: str, **kwargs):
34+
r"""Sends a GET request. Returns :class:`Response` object.
35+
36+
:param url: URL for the new :class:`Request` object.
37+
:param \*\*kwargs: Optional arguments that ``request`` takes.
38+
:rtype: requests.Response
39+
"""
40+
return super().get(self._graph_url(url))
41+
42+
@middleware_control.get_middleware_options
43+
def post(self, url, data=None, json=None, **kwargs):
44+
r"""Sends a POST request. Returns :class:`Response` object.
45+
46+
:param url: URL for the new :class:`Request` object.
47+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
48+
object to send in the body of the :class:`Request`.
49+
:param json: (optional) json to send in the body of the :class:`Request`.
50+
:param \*\*kwargs: Optional arguments that ``request`` takes.
51+
:rtype: requests.Response
52+
"""
53+
return super().post(self._graph_url(url), data, json, **kwargs)
54+
55+
@middleware_control.get_middleware_options
56+
def put(self, url, data=None, **kwargs):
57+
r"""Sends a PUT request. Returns :class:`Response` object.
58+
59+
:param url: URL for the new :class:`Request` object.
60+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
61+
object to send in the body of the :class:`Request`.
62+
:param \*\*kwargs: Optional arguments that ``request`` takes.
63+
:rtype: requests.Response
64+
"""
65+
return super().put(self._graph_url(url), data, **kwargs)
66+
67+
@middleware_control.get_middleware_options
68+
def patch(self, url, data=None, **kwargs):
69+
r"""Sends a PATCH request. Returns :class:`Response` object.
70+
71+
:param url: URL for the new :class:`Request` object.
72+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
73+
object to send in the body of the :class:`Request`.
74+
:param \*\*kwargs: Optional arguments that ``request`` takes.
75+
:rtype: requests.Response
76+
"""
77+
return super().patch(self._graph_url(url), data, **kwargs)
78+
79+
@middleware_control.get_middleware_options
80+
def delete(self, url, **kwargs):
81+
r"""Sends a DELETE request. Returns :class:`Response` object.
82+
83+
:param url: URL for the new :class:`Request` object.
84+
:param \*\*kwargs: Optional arguments that ``request`` takes.
85+
:rtype: requests.Response
86+
"""
87+
return super().delete(url, **kwargs)
88+
89+
def _graph_url(self, url: str) -> str:
90+
"""Appends BASE_URL to user provided path
91+
92+
:param url: user provided path
93+
:return: graph_url
94+
"""
4295
return self._base_url+url if (url[0] == '/') else url
4396

4497
def _register(self, middleware: [BaseMiddleware]) -> None:
98+
"""Adds middleware to middleware_pipeline
99+
100+
:param middleware: list of middleware
101+
"""
45102
if middleware:
46-
middleware_adapter = MiddlewarePipeline()
103+
middleware_pipeline = MiddlewarePipeline()
47104

48105
for ware in middleware:
49-
middleware_adapter.add_middleware(ware)
50-
51-
self.mount('https://', middleware_adapter)
52-
53-
def _prepare_and_send_request(self, method: str = '', url: str = '', **kwargs) -> Response:
54-
# Retrieve middleware options
55-
list_of_scopes = kwargs.pop('scopes', None)
56-
57-
# Prepare request
58-
request_url = self._get_url(url)
59-
request = Request(method, request_url, kwargs)
60-
prepared_request = self.prepare_request(request)
61-
62-
if list_of_scopes is not None:
63-
# Append middleware options to the request object, will be used by MiddlewareController
64-
prepared_request.scopes = list_of_scopes
106+
middleware_pipeline.add_middleware(ware)
65107

66-
return self.send(prepared_request, **kwargs)
108+
self.mount('https://', middleware_pipeline)

msgraphcore/middleware/_middleware.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
from requests.adapters import HTTPAdapter
44
from urllib3 import PoolManager
55

6-
from .options.auth_middleware_options import AuthMiddlewareOptions
7-
from .options.middleware_control import MiddlewareControl
8-
from ..constants import AUTH_MIDDLEWARE_OPTIONS
9-
106

117
class MiddlewarePipeline(HTTPAdapter):
8+
"""MiddlewarePipeline, entry point of middleware
9+
10+
The pipeline is implemented as a linked-list, read more about
11+
it here https://buffered.dev/middleware-python-requests/
12+
"""
1213
def __init__(self):
1314
super().__init__()
1415
self._middleware = None
@@ -21,31 +22,22 @@ def add_middleware(self, middleware):
2122
self._middleware = middleware
2223

2324
def send(self, request, **kwargs):
24-
args = self._attach_middleware_control(request, **kwargs)
25-
2625
if self._middleware_present():
27-
return self._middleware.send(request, **args)
26+
return self._middleware.send(request, **kwargs)
2827
# No middleware in pipeline, call superclass' send
29-
return super().send(request, **args)
30-
31-
def _attach_middleware_control(self, request, **kwargs):
32-
request.middleware_control = MiddlewareControl()
33-
34-
try:
35-
scopes = request.scopes
36-
auth_middleware_options = AuthMiddlewareOptions(scopes)
37-
request.middleware_control.set(AUTH_MIDDLEWARE_OPTIONS, auth_middleware_options)
38-
except KeyError:
39-
# do nothing for now
40-
pass
41-
finally:
42-
return kwargs
28+
return super().send(request, **kwargs)
4329

4430
def _middleware_present(self):
4531
return self._middleware
4632

4733

4834
class BaseMiddleware(HTTPAdapter):
35+
"""Base class for middleware
36+
37+
Handles moving a Request to the next middleware in the pipeline.
38+
If the current middleware is the last one in the pipeline, it
39+
makes a network request
40+
"""
4941
def __init__(self):
5042
super().__init__()
5143
self.next = None

msgraphcore/middleware/authorization.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from ._base_auth import AuthProviderBase, TokenCredential
22
from ..constants import AUTH_MIDDLEWARE_OPTIONS
33
from ._middleware import BaseMiddleware
4+
from .options.middleware_control import middleware_control
45

56

67
class AuthorizationHandler(BaseMiddleware):
@@ -10,22 +11,25 @@ def __init__(self, auth_provider: AuthProviderBase):
1011
self.retry_count = 0
1112

1213
def send(self, request, **kwargs):
13-
options = self._get_middleware_options(request)
14+
# Checks if there are any options for this middleware
15+
options = self._get_middleware_options()
16+
# If there is, get the scopes from the options
1417
if options:
1518
self.auth_provider.scopes = options.scopes
1619

1720
token = self.auth_provider.get_access_token()
1821
request.headers.update({'Authorization': 'Bearer {}'.format(token)})
1922
response = super().send(request, **kwargs)
2023

24+
# Token might have expired just before transmission, retry the request
2125
if response.status_code == 401 and self.retry_count < 2:
2226
self.retry_count += 1
2327
return self.send(request, **kwargs)
2428

2529
return response
2630

27-
def _get_middleware_options(self, request):
28-
return request.middleware_control.get(AUTH_MIDDLEWARE_OPTIONS)
31+
def _get_middleware_options(self):
32+
return middleware_control.get(AUTH_MIDDLEWARE_OPTIONS)
2933

3034

3135
class TokenCredentialAuthProvider(AuthProviderBase):
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11

22
class AuthMiddlewareOptions:
3-
def __init__(self, scopes: str):
3+
def __init__(self, scopes: [str]):
44
self.scopes = scopes
5-

msgraphcore/middleware/options/middleware_control.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from msgraphcore.constants import AUTH_MIDDLEWARE_OPTIONS
2+
from ..options.auth_middleware_options import AuthMiddlewareOptions
3+
4+
15
class MiddlewareControl:
26
def __init__(self):
37
self.middleware_options = {}
@@ -7,3 +11,22 @@ def set(self, middleware_option_name, middleware_option):
711

812
def get(self, middleware_option_name):
913
return self.middleware_options.get(middleware_option_name, None)
14+
15+
def get_middleware_options(self, func):
16+
self._reset_middleware_options()
17+
18+
def wrapper(*args, **kwargs):
19+
# Get middleware options from **kwargs
20+
scopes = kwargs.pop('scopes', None)
21+
if scopes:
22+
# Set middleware options, for use by middleware in the middleware pipeline
23+
self.set(AUTH_MIDDLEWARE_OPTIONS, AuthMiddlewareOptions(scopes))
24+
return func(*args, **kwargs)
25+
return wrapper
26+
27+
def _reset_middleware_options(self):
28+
# Reset middleware, so that they are not persisted across requests
29+
self.middleware_options = {}
30+
31+
32+
middleware_control = MiddlewareControl()

samples/samples.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import json
2+
from pprint import pprint
3+
4+
from azure.identity import InteractiveBrowserCredential
5+
from msgraphcore.middleware.authorization import TokenCredentialAuthProvider
6+
7+
from msgraphcore import GraphSession
8+
9+
browser_credential = InteractiveBrowserCredential(client_id='ENTER_YOUR_CLIENT_ID')
10+
auth_provider = TokenCredentialAuthProvider(browser_credential)
11+
graph_session = GraphSession(auth_provider)
12+
13+
14+
def post_sample():
15+
body = {
16+
'message': {
17+
'subject': 'Python SDK Meet for lunch?',
18+
'body': {
19+
'contentType': 'Text',
20+
'content': 'The new cafeteria is open.'
21+
},
22+
'toRecipients': [
23+
{
24+
'emailAddress': {
25+
'address': 'ENTER_RECEPIENT_EMAIL_ADDRESS'
26+
}
27+
}
28+
]}
29+
}
30+
31+
result = graph_session \
32+
.post('/me/sendMail',
33+
data=json.dumps(body),
34+
scopes=['mail.send'],
35+
headers={'Content-Type': 'application/json'}
36+
)
37+
pprint(result.status_code)
38+
39+
40+
def get_sample():
41+
result = graph_session.get('/me/messages', scopes=['mail.read'])
42+
pprint(result.json())
43+
44+
45+
if __name__ == '__main__':
46+
post_sample()
47+
get_sample()
48+

0 commit comments

Comments
 (0)