diff --git a/Pipfile b/Pipfile index 357132ac..e14cf569 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] # Packages required to run the application microsoft-kiota-abstractions = "==0.1.0" -microsoft-kiota-http = "==0.1.2" +microsoft-kiota-http = "==0.2.0" microsoft-kiota-authentication-azure = "==0.1.0" httpx = {version = "==0.23.0", extras = ["http2"]} diff --git a/Pipfile.lock b/Pipfile.lock index 6ed5a01f..8e5dcb7e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ce3368503cf713b3415a8582cfd0bd6018dd6f620722ff83f461a32d50af7295" + "sha256": "bf905ef0b9652ce4a74de976c703ca85f10d9a3ca25515f2f304aa36a36b8dc4" }, "pipfile-spec": 6, "requires": {}, @@ -288,11 +288,11 @@ }, "microsoft-kiota-http": { "hashes": [ - "sha256:3f1eb3ded4e1db34785636a1633c8584e109093636459a1b45283b2790e159db", - "sha256:539955502a19e74b83a3d4681a5207ce1673eaf023a1a01a62447b6f018bfd9f" + "sha256:12392f4d270001dfeba0464e602ef17922e9990cd6db1e26746b1c9d5263cbbb", + "sha256:34f61b0b732df4875f8f7058907af8f22fa6bc919aff9dc70dd7a806288ee7ab" ], "index": "pypi", - "version": "==0.1.2" + "version": "==0.2.0" }, "multidict": { "hashes": [ @@ -678,35 +678,35 @@ }, "cryptography": { "hashes": [ - "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", - "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", - "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", - "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", - "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", - "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", - "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", - "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", - "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", - "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", - "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", - "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", - "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", - "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", - "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", - "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", - "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", - "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", - "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", - "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", - "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", - "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", - "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", - "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", - "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", - "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" + "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d", + "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd", + "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146", + "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7", + "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436", + "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0", + "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828", + "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b", + "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55", + "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36", + "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50", + "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2", + "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a", + "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8", + "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0", + "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548", + "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320", + "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748", + "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249", + "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959", + "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f", + "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0", + "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd", + "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220", + "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", + "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" ], "markers": "python_version >= '3.6'", - "version": "==38.0.1" + "version": "==38.0.3" }, "dill": { "hashes": [ diff --git a/msgraph/core/__init__.py b/msgraph/core/__init__.py index a425d0c7..c3fa5860 100644 --- a/msgraph/core/__init__.py +++ b/msgraph/core/__init__.py @@ -4,7 +4,6 @@ # ------------------------------------ from ._constants import SDK_VERSION from ._enums import APIVersion, NationalClouds -from .graph_client import GraphClient from .graph_client_factory import GraphClientFactory __version__ = SDK_VERSION diff --git a/msgraph/core/graph_client.py b/msgraph/core/graph_client.py deleted file mode 100644 index 00982f76..00000000 --- a/msgraph/core/graph_client.py +++ /dev/null @@ -1,301 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json -from typing import List, Optional - -import httpx -from kiota_abstractions.authentication import AccessTokenProvider -from kiota_http.middleware.middleware import BaseMiddleware - -from msgraph.core import middleware - -from ._enums import APIVersion, NationalClouds -from .graph_client_factory import GraphClientFactory - - -def collect_options(func): - """Collect middleware options into a middleware control dict and pass it as a header""" - - def wrapper(*args, **kwargs): - - # These are middleware options that can be configured per request. - # Supports options for default middleware as well as custom middleware. - - options = kwargs.pop('request_options', None) - if options: - if 'headers' in kwargs: - kwargs['headers'].update({'request_options': json.dumps(options)}) - else: - kwargs['headers'] = {'request_options': json.dumps(options)} - - return func(*args, **kwargs) - - return wrapper - - -class GraphClient: - """Constructs a custom HTTPClient to be used for requests against Microsoft Graph - - Args: - token_provider (AccessTokenProvider): Used to acquire an access token for the - Microsoft Graph API. - api_version (APIVersion): The Microsoft Graph API version to be used, for example - `APIVersion.v1` (default). This value is used in setting - the base url for all requests for that session. - base_url (NationalClouds): a supported Microsoft Graph cloud endpoint. - timeout (httpx.Timeout):Default connection and read timeout values for all session - requests.Specify a tuple in the form of httpx.Timeout( - REQUEST_TIMEOUT, connect=CONNECTION_TIMEOUT), - client (Optional[httpx.AsyncClient]): A custom AsynClient instance from the - python httpx library - middleware (BaseMiddlware): Custom middleware list that will be used to create - a middleware pipeline. The middleware should be arranged in the order in which they will - modify the request. - """ - DEFAULT_CONNECTION_TIMEOUT: int = 30 - DEFAULT_REQUEST_TIMEOUT: int = 100 - __instance = None - - def __new__(cls, *args, **kwargs): - if not GraphClient.__instance: - GraphClient.__instance = object.__new__(cls) - return GraphClient.__instance - - def __init__( - self, - token_provider: Optional[AccessTokenProvider] = None, - api_version: APIVersion = APIVersion.v1, - base_url: NationalClouds = NationalClouds.Global, - timeout: httpx.Timeout = httpx.Timeout( - DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT - ), - client: Optional[httpx.AsyncClient] = None, - middleware: Optional[List[BaseMiddleware]] = None - ): - """ - Class constructor that accepts a session object and kwargs to - be passed to the GraphClientFactory - """ - self.client = self._get_graph_client( - token_provider, api_version, base_url, timeout, client, middleware - ) - - @collect_options - async def get( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - r"""Sends a GET request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - async with self.client as client: - return await client.get( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - @collect_options - async def options( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - r"""Sends a OPTIONS request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - async with self.client as client: - return await client.options( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - @collect_options - async def head( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - r"""Sends a HEAD request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - async with self.client as client: - return await client.head( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - @collect_options - async def post( - self, - url, - *, - content=None, - data=None, - files=None, - json=None, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - r"""Sends a POST request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param json: (optional) json to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - async with self.client as client: - return await client.post( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - extensions=extensions - ) - - @collect_options - async def put( - self, - url, - *, - content=None, - data=None, - files=None, - json=None, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - r"""Sends a PUT request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - async with self.client as client: - return await client.put( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - extensions=extensions - ) - - @collect_options - async def patch( - self, - url, - *, - content=None, - data=None, - files=None, - json=None, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - r"""Sends a PATCH request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - async with self.client as client: - return await client.patch( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - extensions=extensions - ) - - @collect_options - async def delete( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - r"""Sends a DELETE request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - async with self.client as client: - return await client.delete( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - def _get_graph_client( - self, token_provider: Optional[AccessTokenProvider], api_version: APIVersion, - base_url: NationalClouds, timeout: httpx.Timeout, client: Optional[httpx.AsyncClient], - middleware: Optional[List[BaseMiddleware]] - ): - """Method to always return a single instance of a HTTP Client""" - if not client: - client = httpx.AsyncClient( - base_url=self._get_base_url(base_url, api_version), timeout=timeout, http2=True - ) - if token_provider and middleware: - raise ValueError( - "Invalid parameters! Both TokenCredential and middleware cannot be passed" - ) - - if middleware: - return GraphClientFactory.create_with_custom_middleware( - client=client, middleware=middleware - ) - return GraphClientFactory.create_with_default_middleware( - client=client, token_provider=token_provider - ) - - def _get_base_url(self, base_url: str, api_version: APIVersion) -> str: - """Helper method to set the complete base url""" - base_url = f'{base_url}/{api_version}' - return base_url diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index fa6cc843..6cbbbcf7 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -7,18 +7,12 @@ from typing import List, Optional import httpx -from kiota_abstractions.authentication import AccessTokenProvider from kiota_http.kiota_client_factory import KiotaClientFactory from kiota_http.middleware import AsyncKiotaTransport from kiota_http.middleware.middleware import BaseMiddleware -from .middleware import ( - GraphAuthorizationHandler, - GraphMiddlewarePipeline, - GraphRedirectHandler, - GraphRetryHandler, - GraphTelemetryHandler, -) +from ._enums import APIVersion, NationalClouds +from .middleware import GraphTelemetryHandler class GraphClientFactory(KiotaClientFactory): @@ -28,31 +22,34 @@ class GraphClientFactory(KiotaClientFactory): @staticmethod def create_with_default_middleware( - client: httpx.AsyncClient, - token_provider: Optional[AccessTokenProvider] = None + api_version: APIVersion = APIVersion.v1, + host: NationalClouds = NationalClouds.Global ) -> httpx.AsyncClient: """Constructs native HTTP AsyncClient(httpx.AsyncClient) instances configured with a custom transport loaded with a default pipeline of middleware. Returns: httpx.AsycClient: An instance of the AsyncClient object """ + client = KiotaClientFactory.get_default_client() + client.base_url = GraphClientFactory._get_base_url(host, api_version) current_transport = client._transport - middleware = GraphClientFactory._get_common_middleware() - if token_provider: - middleware.insert(0, GraphAuthorizationHandler(token_provider)) - middleware_pipeline = GraphClientFactory._create_middleware_pipeline( + middleware = KiotaClientFactory.get_default_middleware() + middleware.append(GraphTelemetryHandler()) + middleware_pipeline = KiotaClientFactory.create_middleware_pipeline( middleware, current_transport ) client._transport = AsyncKiotaTransport( - transport=current_transport, middleware=middleware_pipeline + transport=current_transport, pipeline=middleware_pipeline ) return client @staticmethod def create_with_custom_middleware( - client: httpx.AsyncClient, middleware: Optional[List[BaseMiddleware]] + middleware: Optional[List[BaseMiddleware]], + api_version: APIVersion = APIVersion.v1, + host: NationalClouds = NationalClouds.Global, ) -> httpx.AsyncClient: """Applies a custom middleware chain to the HTTP Client @@ -61,34 +58,21 @@ def create_with_custom_middleware( a middleware pipeline. The middleware should be arranged in the order in which they will modify the request. """ + client = KiotaClientFactory.get_default_client() + client.base_url = GraphClientFactory._get_base_url(host, api_version) current_transport = client._transport - middleware_pipeline = GraphClientFactory._create_middleware_pipeline( + + middleware_pipeline = KiotaClientFactory.create_middleware_pipeline( middleware, current_transport ) client._transport = AsyncKiotaTransport( - transport=current_transport, middleware=middleware_pipeline + transport=current_transport, pipeline=middleware_pipeline ) return client @staticmethod - def _get_common_middleware() -> List[BaseMiddleware]: - """ - Helper method that returns a list of cross cutting middleware - """ - middleware = [GraphRedirectHandler(), GraphRetryHandler(), GraphTelemetryHandler()] - - return middleware - - @staticmethod - def _create_middleware_pipeline( - middleware: Optional[List[BaseMiddleware]], transport: httpx.AsyncBaseTransport - ) -> GraphMiddlewarePipeline: - """ - Helper method that constructs a middleware_pipeline with the specified middleware - """ - middleware_pipeline = GraphMiddlewarePipeline(transport) - if middleware: - for ware in middleware: - middleware_pipeline.add_middleware(ware) - return middleware_pipeline + def _get_base_url(host: str, api_version: APIVersion) -> str: + """Helper method to set the complete base url""" + base_url = f'{host}/{api_version}' + return base_url diff --git a/msgraph/core/graph_request_adapter.py b/msgraph/core/graph_request_adapter.py index ca88e263..81c458fb 100644 --- a/msgraph/core/graph_request_adapter.py +++ b/msgraph/core/graph_request_adapter.py @@ -1,8 +1,6 @@ +import httpx from kiota_abstractions.authentication import AuthenticationProvider from kiota_abstractions.serialization import ( - Parsable, - ParsableFactory, - ParseNode, ParseNodeFactory, ParseNodeFactoryRegistry, SerializationWriterFactory, @@ -10,7 +8,6 @@ ) from kiota_http.httpx_request_adapter import HttpxRequestAdapter -from .graph_client import GraphClient from .graph_client_factory import GraphClientFactory @@ -22,7 +19,7 @@ def __init__( parse_node_factory: ParseNodeFactory = ParseNodeFactoryRegistry(), serialization_writer_factory: SerializationWriterFactory = SerializationWriterFactoryRegistry(), - http_client: GraphClient = GraphClient() + http_client: httpx.AsyncClient = GraphClientFactory.create_with_default_middleware() ) -> None: super().__init__( authentication_provider=authentication_provider, diff --git a/msgraph/core/middleware/__init__.py b/msgraph/core/middleware/__init__.py index 2cdae665..508a0819 100644 --- a/msgraph/core/middleware/__init__.py +++ b/msgraph/core/middleware/__init__.py @@ -2,9 +2,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from .authorization import GraphAuthorizationHandler -from .middleware import GraphMiddlewarePipeline, GraphRequest -from .redirect import GraphRedirectHandler from .request_context import GraphRequestContext -from .retry import GraphRetryHandler from .telemetry import GraphTelemetryHandler diff --git a/msgraph/core/middleware/authorization.py b/msgraph/core/middleware/authorization.py deleted file mode 100644 index e3848570..00000000 --- a/msgraph/core/middleware/authorization.py +++ /dev/null @@ -1,40 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import TypeVar - -import httpx -from kiota_abstractions.authentication import AccessTokenProvider -from kiota_http.middleware.middleware import BaseMiddleware - -from .._enums import FeatureUsageFlag -from .middleware import GraphRequest - - -class GraphAuthorizationHandler(BaseMiddleware): - """ - Transparently authorize requests by adding authorization header to the request - """ - - def __init__(self, token_provider: AccessTokenProvider): - """Constructor for authorization handler - - Args: - auth_provider (AuthenticationProvider): AuthorizationProvider instance - that will be used to fetch the token - """ - super().__init__() - - self.token_provider = token_provider - - async def send( - self, request: GraphRequest, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: - - request.context.feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED - - token = await self.token_provider.get_authorization_token(str(request.url)) - request.headers.update({'Authorization': f'Bearer {token}'}) - response = await super().send(request, transport) - return response diff --git a/msgraph/core/middleware/middleware.py b/msgraph/core/middleware/middleware.py deleted file mode 100644 index e6ae0043..00000000 --- a/msgraph/core/middleware/middleware.py +++ /dev/null @@ -1,46 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json - -import httpx -from kiota_http.middleware import MiddlewarePipeline - -from .request_context import GraphRequestContext - - -class GraphRequest(httpx.Request): - """Http Request object with a custom request context - """ - context: GraphRequestContext - - -class GraphMiddlewarePipeline(MiddlewarePipeline): - """Chain of graph specific middleware that process requests in the same order - and responses in reverse order to requests. The pipeline is implemented as a linked-list - """ - - async def send(self, request: GraphRequest) -> httpx.Response: - """Passes the request to the next middleware if available or makes a network request - - Args: - request (httpx.Request): The http request - - Returns: - httpx.Response: The http response - """ - - request_options = {} - options = request.headers.pop('request_options', None) - if options: - request_options = json.loads(options) - - request.context = GraphRequestContext(request_options, request.headers) - - if self._middleware_present(): - return await self._first_middleware.send(request, self._transport) - # No middleware in pipeline, send the request. - response = await self._transport.handle_async_request(request) - response.request = request - return response diff --git a/msgraph/core/middleware/redirect.py b/msgraph/core/middleware/redirect.py deleted file mode 100644 index 82206a6c..00000000 --- a/msgraph/core/middleware/redirect.py +++ /dev/null @@ -1,38 +0,0 @@ -import typing -from http import client - -import httpx -from kiota_http.middleware import BaseMiddleware, RedirectHandler - -from .._enums import FeatureUsageFlag -from .middleware import GraphRequest - - -class GraphRedirectHandler(RedirectHandler): - """Middleware designed to handle 3XX responses transparently - """ - - async def send( - self, request: GraphRequest, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: - """Sends the http request object to the next middleware or redirects - the request if necessary. - """ - request.context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED - - retryable = True - while retryable: - response = await super(RedirectHandler, self).send(request, transport) - redirect_location = self.get_redirect_location(response) - if redirect_location and self.should_redirect: - retryable = self.increment(response) - new_request = self._build_redirect_request(request, response) - new_request.context = request.context - request = new_request - - continue - - response.history = self.history - return response - - raise Exception(f"Too many redirects. {response.history}") diff --git a/msgraph/core/middleware/retry.py b/msgraph/core/middleware/retry.py deleted file mode 100644 index 0c85ca23..00000000 --- a/msgraph/core/middleware/retry.py +++ /dev/null @@ -1,48 +0,0 @@ -import time - -import httpx -from kiota_http.middleware import RetryHandler - -from .._enums import FeatureUsageFlag -from .middleware import GraphRequest - - -class GraphRetryHandler(RetryHandler): - """ - Middleware that handles failed requests - """ - - async def send(self, request: GraphRequest, transport: httpx.AsyncBaseTransport): - """ - Sends the http request object to the next middleware or retries the request if necessary. - """ - response = None - retry_count = 0 - retry_valid = self.retries_allowed - - request.context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED - - while retry_valid: - start_time = time.time() - if retry_count > 0: - request.headers.update({'retry-attempt': f'{retry_count}'}) - response = await super(RetryHandler, self).send(request, transport) - # Check if the request needs to be retried based on the response method - # and status code - if self.should_retry(request, response): - # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_count) - - # Get the delay time between retries - delay = self.get_delay_time(retry_count, response) - - if retry_valid and delay < self.timeout: - time.sleep(delay) - end_time = time.time() - self.timeout -= (end_time - start_time) - # increment the count for retries - retry_count += 1 - - continue - break - return response diff --git a/msgraph/core/middleware/telemetry.py b/msgraph/core/middleware/telemetry.py index 8c131be2..01d66f9a 100644 --- a/msgraph/core/middleware/telemetry.py +++ b/msgraph/core/middleware/telemetry.py @@ -1,12 +1,18 @@ +import http +import json import platform import httpx -from kiota_http.middleware.middleware import BaseMiddleware +from kiota_http.middleware import AsyncKiotaTransport, BaseMiddleware, RedirectHandler, RetryHandler from urllib3.util import parse_url from .._constants import SDK_VERSION -from .._enums import NationalClouds -from .middleware import GraphRequest +from .._enums import FeatureUsageFlag, NationalClouds +from .request_context import GraphRequestContext + + +class GraphRequest(httpx.Request): + context: GraphRequestContext class GraphTelemetryHandler(BaseMiddleware): @@ -14,11 +20,11 @@ class GraphTelemetryHandler(BaseMiddleware): the SDK team improve the developer experience. """ - async def send( - self, request: GraphRequest, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: + async def send(self, request: GraphRequest, transport: AsyncKiotaTransport): """Adds telemetry headers and sends the http request. """ + self.set_request_context_and_feature_usage(request, transport) + if self.is_graph_url(request.url): self._add_client_request_id_header(request) self._append_sdk_version_header(request) @@ -28,6 +34,27 @@ async def send( response = await super().send(request, transport) return response + def set_request_context_and_feature_usage( + self, request: GraphRequest, transport: AsyncKiotaTransport + ) -> GraphRequest: + + request_options = {} + options = request.headers.pop('request_options', None) + if options: + request_options = json.loads(options) + + request.context = GraphRequestContext(request_options, request.headers) + middleware = transport.pipeline._first_middleware + while middleware: + if isinstance(middleware, RedirectHandler): + request.context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + if isinstance(middleware, RetryHandler): + request.context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED + + middleware = middleware.next + + return request + def is_graph_url(self, url): """Check if the request is made to a graph endpoint. We do not add telemetry headers to non-graph endpoints""" diff --git a/tests/conftest.py b/tests/conftest.py index 6bdd4c41..f77d5e91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,44 +1,42 @@ import httpx import pytest -from azure.identity.aio import DefaultAzureCredential -from kiota_abstractions.authentication import AccessTokenProvider +from kiota_abstractions.authentication import AnonymousAuthenticationProvider from kiota_authentication_azure.azure_identity_access_token_provider import ( AzureIdentityAccessTokenProvider, ) from msgraph.core import APIVersion, NationalClouds -from msgraph.core.middleware import GraphRequest, GraphRequestContext +from msgraph.core.graph_client_factory import GraphClientFactory +from msgraph.core.middleware import GraphRequestContext BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 -class MockAccessTokenProvider(AccessTokenProvider): +class MockAuthenticationProvider(AnonymousAuthenticationProvider): - async def get_authorization_token(self, request: GraphRequest) -> str: + async def get_authorization_token(self, request: httpx.Request) -> str: """Returns a string representing a dummy token Args: request (GraphRequest): Graph request object """ - return "Sample token" - - def get_allowed_hosts_validator(self) -> None: - pass + request.headers['Authorization'] = 'Sample token' + return @pytest.fixture -def mock_token_provider(): - return MockAccessTokenProvider() +def mock_auth_provider(): + return MockAuthenticationProvider() @pytest.fixture def mock_transport(): - return httpx.AsyncClient()._transport + client = GraphClientFactory.create_with_default_middleware() + return client._transport @pytest.fixture def mock_request(): req = httpx.Request('GET', "https://example.org") - req.context = GraphRequestContext({}, req.headers) return req diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index b74cfa3b..00000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ diff --git a/tests/unit/test_graph_auth_handler.py b/tests/unit/test_graph_auth_handler.py deleted file mode 100644 index a2467b73..00000000 --- a/tests/unit/test_graph_auth_handler.py +++ /dev/null @@ -1,25 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import httpx -import pytest -from kiota_abstractions.authentication import AccessTokenProvider - -from msgraph.core._enums import FeatureUsageFlag -from msgraph.core.middleware import GraphAuthorizationHandler, GraphRequestContext - - -def test_auth_handler_initialization(mock_token_provider): - auth_handler = GraphAuthorizationHandler(mock_token_provider) - assert isinstance(auth_handler.token_provider, AccessTokenProvider) - - -@pytest.mark.trio -async def test_auth_handler_send(mock_token_provider, mock_request, mock_transport): - auth_handler = GraphAuthorizationHandler(mock_token_provider) - resp = await auth_handler.send(mock_request, mock_transport) - assert isinstance(resp, httpx.Response) - assert resp.status_code == 200 - assert 'Authorization' in resp.request.headers - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.AUTH_HANDLER_ENABLED) diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py deleted file mode 100644 index 55da17fe..00000000 --- a/tests/unit/test_graph_client.py +++ /dev/null @@ -1,80 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import httpx -import pytest -from asyncmock import AsyncMock - -from msgraph.core import APIVersion, GraphClient, NationalClouds -from msgraph.core.middleware.authorization import GraphAuthorizationHandler - - -def test_initialize_graph_client_with_default_middleware(mock_token_provider): - """ - Test creating a graph client with default middleware works as expected - """ - - graph_client = GraphClient(token_provider=mock_token_provider) - - assert isinstance(graph_client.client, httpx.AsyncClient) - assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' - - -def test_initialize_graph_client_with_custom_middleware(mock_token_provider): - """ - Test creating a graph client with custom middleware works as expected - """ - middleware = [ - GraphAuthorizationHandler(token_provider=mock_token_provider), - ] - graph_client = GraphClient(middleware=middleware) - - assert isinstance(graph_client.client, httpx.AsyncClient) - assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' - - -def test_initialize_graph_client_both_token_provider_and_custom_middleware(mock_token_provider): - """ - Test creating a graph client with both token provider and custom middleware throws an error - """ - middleware = [ - GraphAuthorizationHandler(token_provider=mock_token_provider), - ] - with pytest.raises(Exception): - graph_client = GraphClient(token_provider=mock_token_provider, middleware=middleware) - - -def test_graph_client_with_custom_configuration(mock_token_provider): - """ - Test creating a graph client with custom middleware works as expected - """ - graph_client = GraphClient( - token_provider=mock_token_provider, - api_version=APIVersion.beta, - base_url=NationalClouds.China - ) - - assert str(graph_client.client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' - - -def test_graph_client_uses_same_session(mock_token_provider): - """ - Test graph client is a singleton class and uses the same session - """ - graph_client1 = GraphClient(token_provider=mock_token_provider) - - graph_client2 = GraphClient(token_provider=mock_token_provider) - assert graph_client1 is graph_client2 - - -def test_get_base_url(): - """ - Test base url is formed by combining the national cloud endpoint with - Api version - """ - url = GraphClient()._get_base_url( - base_url=NationalClouds.Germany, - api_version=APIVersion.beta, - ) - assert url == f'{NationalClouds.Germany}/{APIVersion.beta}' diff --git a/tests/unit/test_graph_client_factory.py b/tests/unit/test_graph_client_factory.py index 9195c4c6..f6746fbd 100644 --- a/tests/unit/test_graph_client_factory.py +++ b/tests/unit/test_graph_client_factory.py @@ -4,62 +4,55 @@ # ------------------------------------ import httpx import pytest -from kiota_http.middleware import AsyncKiotaTransport +from kiota_http.middleware import AsyncKiotaTransport, MiddlewarePipeline, RedirectHandler from msgraph.core import APIVersion, GraphClientFactory, NationalClouds -from msgraph.core._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from msgraph.core.middleware import GraphAuthorizationHandler -from msgraph.core.middleware.middleware import GraphMiddlewarePipeline -from msgraph.core.middleware.redirect import GraphRedirectHandler -from msgraph.core.middleware.retry import GraphRetryHandler from msgraph.core.middleware.telemetry import GraphTelemetryHandler -def test_create_with_default_middleware_no_auth_provider(): - """Test creation of GraphClient without a token provider does not - add the Authorization middleware""" - client = GraphClientFactory.create_with_default_middleware(client=httpx.AsyncClient()) +def test_create_with_default_middleware(): + """Test creation of GraphClient using default middleware""" + client = GraphClientFactory.create_with_default_middleware() assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) - pipeline = client._transport.middleware - assert isinstance(pipeline, GraphMiddlewarePipeline) - assert not isinstance(pipeline._first_middleware, GraphAuthorizationHandler) + pipeline = client._transport.pipeline + assert isinstance(pipeline, MiddlewarePipeline) + assert isinstance(pipeline._first_middleware, RedirectHandler) + assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) -def test_create_with_default_middleware(mock_token_provider): - """Test creation of GraphClient using default middleware and passing a token - provider adds Authorization middleware""" - client = GraphClientFactory.create_with_default_middleware( - client=httpx.AsyncClient(), token_provider=mock_token_provider - ) - - assert isinstance(client, httpx.AsyncClient) - assert isinstance(client._transport, AsyncKiotaTransport) - pipeline = client._transport.middleware - assert isinstance(pipeline, GraphMiddlewarePipeline) - assert isinstance(pipeline._first_middleware, GraphAuthorizationHandler) - - -def test_create_with_custom_middleware(mock_token_provider): +def test_create_with_custom_middleware(): """Test creation of HTTP Clients with custom middleware""" middleware = [ GraphTelemetryHandler(), ] - client = GraphClientFactory.create_with_custom_middleware( - client=httpx.AsyncClient(), middleware=middleware - ) + client = GraphClientFactory.create_with_custom_middleware(middleware=middleware) assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) - pipeline = client._transport.middleware + pipeline = client._transport.pipeline assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) -def test_get_common_middleware(): - middleware = GraphClientFactory._get_common_middleware() - - assert len(middleware) == 3 - assert isinstance(middleware[0], GraphRedirectHandler) - assert isinstance(middleware[1], GraphRetryHandler) - assert isinstance(middleware[2], GraphTelemetryHandler) +def test_graph_client_factory_with_custom_configuration(): + """ + Test creating a graph client with custom url overrides the default + """ + graph_client = GraphClientFactory.create_with_default_middleware( + api_version=APIVersion.beta, host=NationalClouds.China + ) + assert isinstance(graph_client, httpx.AsyncClient) + assert str(graph_client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' + + +def test_get_base_url(): + """ + Test base url is formed by combining the national cloud endpoint with + Api version + """ + url = GraphClientFactory._get_base_url( + host=NationalClouds.Germany, + api_version=APIVersion.beta, + ) + assert url == f'{NationalClouds.Germany}/{APIVersion.beta}' diff --git a/tests/unit/test_graph_middleware_pipeline.py b/tests/unit/test_graph_middleware_pipeline.py deleted file mode 100644 index 379a2a83..00000000 --- a/tests/unit/test_graph_middleware_pipeline.py +++ /dev/null @@ -1,18 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import httpx -import pytest - -from msgraph.core.middleware import GraphMiddlewarePipeline, GraphRequestContext - - -@pytest.mark.trio -async def test_middleware_pipeline_send(mock_transport, mock_request): - pipeline = GraphMiddlewarePipeline(mock_transport) - response = await pipeline.send(mock_request) - - assert isinstance(response, httpx.Response) - assert 'request_options' not in response.request.headers - assert isinstance(response.request.context, GraphRequestContext) diff --git a/tests/unit/test_graph_redirect_handler.py b/tests/unit/test_graph_redirect_handler.py deleted file mode 100644 index 7eaf1369..00000000 --- a/tests/unit/test_graph_redirect_handler.py +++ /dev/null @@ -1,31 +0,0 @@ -import httpx -import pytest -from kiota_abstractions.authentication import AccessTokenProvider - -from msgraph.core._enums import FeatureUsageFlag -from msgraph.core.middleware import GraphRedirectHandler, GraphRequestContext - - -@pytest.mark.trio -async def test_redirect_handler_send(mock_token_provider, mock_request, mock_transport): - redirect_handler = GraphRedirectHandler() - - req = httpx.Request('GET', "https://httpbin.org/redirect/2") - req.context = GraphRequestContext({}, req.headers) - resp = await redirect_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - assert resp.status_code == 200 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.REDIRECT_HANDLER_ENABLED) - - -@pytest.mark.trio -async def test_redirect_handler_send_max_redirects( - mock_token_provider, mock_request, mock_transport -): - redirect_handler = GraphRedirectHandler() - - req = httpx.Request('GET', "https://httpbin.org/redirect/7") - req.context = GraphRequestContext({}, req.headers) - with pytest.raises(Exception) as e: - resp = await redirect_handler.send(req, mock_transport) diff --git a/tests/unit/test_graph_request_adapter.py b/tests/unit/test_graph_request_adapter.py index 8379c1df..6a0a58e8 100644 --- a/tests/unit/test_graph_request_adapter.py +++ b/tests/unit/test_graph_request_adapter.py @@ -7,12 +7,11 @@ ) from msgraph.core.graph_request_adapter import GraphRequestAdapter -from tests.conftest import mock_token_provider -def test_create_graph_request_adapter(mock_token_provider): - request_adapter = GraphRequestAdapter(mock_token_provider) - assert request_adapter._authentication_provider is mock_token_provider +def test_create_graph_request_adapter(mock_auth_provider): + request_adapter = GraphRequestAdapter(mock_auth_provider) + assert request_adapter._authentication_provider is mock_auth_provider assert isinstance(request_adapter._parse_node_factory, ParseNodeFactoryRegistry) assert isinstance( request_adapter._serialization_writer_factory, SerializationWriterFactoryRegistry diff --git a/tests/unit/test_graph_retry_handler.py b/tests/unit/test_graph_retry_handler.py deleted file mode 100644 index 97ec5988..00000000 --- a/tests/unit/test_graph_retry_handler.py +++ /dev/null @@ -1,90 +0,0 @@ -import httpx -import pytest -from kiota_abstractions.authentication import AccessTokenProvider - -from msgraph.core._enums import FeatureUsageFlag -from msgraph.core.middleware import GraphRequestContext, GraphRetryHandler - -# @pytest.mark.trio -# async def test_redirect_handler_send(mock_token_provider): -# redirect_handler = GraphRetryHandler() - -# req = httpx.Request('GET', "https://httpbin.org/redirect/2") -# req.context = GraphRequestContext({}, req.headers) -# resp = await redirect_handler.send(req, mock_transport) - -# assert isinstance(resp, httpx.Response) -# assert resp.status_code == 302 -# assert resp.request.context.feature_usage == hex(FeatureUsageFlag.REDIRECT_HANDLER_ENABLED) - - -@pytest.mark.trio -async def test_no_retry_success_response(mock_transport): - """ - Test that a request with valid http header and a success response is not retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/200") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - assert resp.status_code == 200 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - with pytest.raises(KeyError): - resp.request.headers["retry-attempt"] - - -@pytest.mark.trio -async def test_valid_retry_429(mock_transport): - """ - Test that a request with valid http header and 503 response is retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/429") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - - assert resp.status_code == 429 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - assert int(resp.request.headers["retry-attempt"]) > 0 - - -@pytest.mark.trio -async def test_valid_retry_503(mock_transport): - """ - Test that a request with valid http header and 503 response is retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/503") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - - assert resp.status_code == 503 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - assert int(resp.request.headers["retry-attempt"]) > 0 - - -@pytest.mark.trio -async def test_valid_retry_504(mock_transport): - """ - Test that a request with valid http header and 503 response is retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/504") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - - assert resp.status_code == 504 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - assert int(resp.request.headers["retry-attempt"]) > 0 diff --git a/tests/unit/test_graph_telemetry_handler.py b/tests/unit/test_graph_telemetry_handler.py index 2bee75f7..b0a24720 100644 --- a/tests/unit/test_graph_telemetry_handler.py +++ b/tests/unit/test_graph_telemetry_handler.py @@ -9,12 +9,23 @@ import httpx import pytest -from msgraph.core import SDK_VERSION, APIVersion, GraphClient, NationalClouds +from msgraph.core import SDK_VERSION, APIVersion, NationalClouds +from msgraph.core._enums import FeatureUsageFlag from msgraph.core.middleware import GraphRequestContext, GraphTelemetryHandler BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 +def test_set_request_context_and_feature_usage(mock_request, mock_transport): + telemetry_handler = GraphTelemetryHandler() + telemetry_handler.set_request_context_and_feature_usage(mock_request, mock_transport) + + assert hasattr(mock_request, 'context') + assert mock_request.context.feature_usage == hex( + FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + ) + + def test_is_graph_url(mock_graph_request): """ Test method that checks whether a request url is a graph endpoint