Skip to content

Commit 90e57b3

Browse files
committed
New folder structure
1 parent 62e33ac commit 90e57b3

12 files changed

+336
-13
lines changed

src/msgraph_core/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# -----------------------------------
5+
6+
# pylint: disable=line-too-long
7+
# This is to allow complete package description on PyPI
8+
"""
9+
Core component of the Microsoft Graph Python SDK consisting of HTTP/Graph Client and a configurable middleware pipeline (Preview).
10+
"""
11+
from ._constants import SDK_VERSION
12+
from ._enums import APIVersion, NationalClouds
13+
from .base_graph_request_adapter import BaseGraphRequestAdapter
14+
from .graph_client_factory import GraphClientFactory
15+
16+
__version__ = SDK_VERSION

src/msgraph_core/_constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
"""
6+
Application constants. All defaults can be changed when initializing a client
7+
via the GraphClient or HttpClientFactory
8+
"""
9+
DEFAULT_REQUEST_TIMEOUT = 100
10+
DEFAULT_CONNECTION_TIMEOUT = 30
11+
SDK_VERSION = '0.2.2'

src/msgraph_core/_enums.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
#pylint: disable=invalid-name
6+
7+
from enum import Enum
8+
9+
10+
class APIVersion(str, Enum):
11+
"""Enumerated list of supported API Versions"""
12+
beta = 'beta'
13+
v1 = 'v1.0'
14+
15+
16+
class FeatureUsageFlag(int, Enum):
17+
"""Enumerated list of values used to flag usage of specific middleware"""
18+
19+
NONE = 0
20+
REDIRECT_HANDLER_ENABLED = 1
21+
RETRY_HANDLER_ENABLED = 2
22+
AUTH_HANDLER_ENABLED = 4
23+
DEFAULT_HTTP_PROVIDER_ENABLED = 8
24+
LOGGING_HANDLER_ENABLED = 16
25+
26+
27+
class NationalClouds(str, Enum):
28+
"""Enumerated list of supported sovereign clouds"""
29+
30+
China = 'https://microsoftgraph.chinacloudapi.cn'
31+
Germany = 'https://graph.microsoft.de'
32+
Global = 'https://graph.microsoft.com'
33+
US_DoD = 'https://dod-graph.microsoft.us'
34+
US_GOV = 'https://graph.microsoft.us'
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import httpx
2+
from kiota_abstractions.authentication import AuthenticationProvider
3+
from kiota_abstractions.serialization import (
4+
ParseNodeFactory,
5+
ParseNodeFactoryRegistry,
6+
SerializationWriterFactory,
7+
SerializationWriterFactoryRegistry,
8+
)
9+
from kiota_http.httpx_request_adapter import HttpxRequestAdapter
10+
11+
from .graph_client_factory import GraphClientFactory
12+
13+
14+
class BaseGraphRequestAdapter(HttpxRequestAdapter):
15+
16+
def __init__(
17+
self,
18+
authentication_provider: AuthenticationProvider,
19+
parse_node_factory: ParseNodeFactory = ParseNodeFactoryRegistry(),
20+
serialization_writer_factory:
21+
SerializationWriterFactory = SerializationWriterFactoryRegistry(),
22+
http_client: httpx.AsyncClient = GraphClientFactory.create_with_default_middleware()
23+
) -> None:
24+
super().__init__(
25+
authentication_provider=authentication_provider,
26+
parse_node_factory=parse_node_factory,
27+
serialization_writer_factory=serialization_writer_factory,
28+
http_client=http_client
29+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
import httpx
10+
from kiota_http.kiota_client_factory import KiotaClientFactory
11+
from kiota_http.middleware import AsyncKiotaTransport
12+
from kiota_http.middleware.middleware import BaseMiddleware
13+
14+
from ._enums import APIVersion, NationalClouds
15+
from .middleware import GraphTelemetryHandler
16+
17+
18+
class GraphClientFactory(KiotaClientFactory):
19+
"""Constructs httpx.AsyncClient instances configured with either custom or default
20+
pipeline of graph specific middleware.
21+
"""
22+
23+
@staticmethod
24+
def create_with_default_middleware(
25+
api_version: APIVersion = APIVersion.v1,
26+
host: NationalClouds = NationalClouds.Global
27+
) -> httpx.AsyncClient:
28+
"""Constructs native HTTP AsyncClient(httpx.AsyncClient) instances configured with
29+
a custom transport loaded with a default pipeline of middleware.
30+
Returns:
31+
httpx.AsycClient: An instance of the AsyncClient object
32+
"""
33+
client = KiotaClientFactory.get_default_client()
34+
client.base_url = GraphClientFactory._get_base_url(host, api_version)
35+
current_transport = client._transport
36+
37+
middleware = KiotaClientFactory.get_default_middleware()
38+
middleware.append(GraphTelemetryHandler())
39+
middleware_pipeline = KiotaClientFactory.create_middleware_pipeline(
40+
middleware, current_transport
41+
)
42+
43+
client._transport = AsyncKiotaTransport(
44+
transport=current_transport, pipeline=middleware_pipeline
45+
)
46+
return client
47+
48+
@staticmethod
49+
def create_with_custom_middleware(
50+
middleware: Optional[List[BaseMiddleware]],
51+
api_version: APIVersion = APIVersion.v1,
52+
host: NationalClouds = NationalClouds.Global,
53+
) -> httpx.AsyncClient:
54+
"""Applies a custom middleware chain to the HTTP Client
55+
56+
Args:
57+
middleware(List[BaseMiddleware]): Custom middleware list that will be used to create
58+
a middleware pipeline. The middleware should be arranged in the order in which they will
59+
modify the request.
60+
"""
61+
client = KiotaClientFactory.get_default_client()
62+
client.base_url = GraphClientFactory._get_base_url(host, api_version)
63+
current_transport = client._transport
64+
65+
middleware_pipeline = KiotaClientFactory.create_middleware_pipeline(
66+
middleware, current_transport
67+
)
68+
69+
client._transport = AsyncKiotaTransport(
70+
transport=current_transport, pipeline=middleware_pipeline
71+
)
72+
return client
73+
74+
@staticmethod
75+
def _get_base_url(host: str, api_version: APIVersion) -> str:
76+
"""Helper method to set the complete base url"""
77+
base_url = f'{host}/{api_version}'
78+
return base_url
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
from .request_context import GraphRequestContext
6+
from .telemetry import GraphTelemetryHandler
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
import uuid
6+
7+
import httpx
8+
9+
from .._enums import FeatureUsageFlag
10+
11+
12+
class GraphRequestContext:
13+
"""A request context contains data that is persisted throughout the request and
14+
includes a ClientRequestId property, MiddlewareControl property to control behavior
15+
of middleware as well as a FeatureUsage property to keep track of middleware used
16+
in making the request.
17+
"""
18+
19+
def __init__(self, middleware_control: dict, headers: httpx.Headers):
20+
"""Constructor for request context instances
21+
22+
Args:
23+
middleware_control (dict): A dictionary of optional middleware options
24+
that can be accessed by middleware components to override the options provided
25+
during middleware initialization,
26+
27+
headers (dict): A dictionary containing the request headers. Used to check for a
28+
user provided client request id.
29+
"""
30+
self.middleware_control = middleware_control
31+
self.client_request_id = headers.get('client-request-id', str(uuid.uuid4()))
32+
self._feature_usage: int = FeatureUsageFlag.NONE
33+
34+
@property
35+
def feature_usage(self):
36+
return hex(self._feature_usage)
37+
38+
@feature_usage.setter
39+
def feature_usage(self, flag: FeatureUsageFlag) -> None:
40+
self._feature_usage = self._feature_usage | flag
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import http
2+
import json
3+
import platform
4+
5+
import httpx
6+
from kiota_http.middleware import AsyncKiotaTransport, BaseMiddleware, RedirectHandler, RetryHandler
7+
from urllib3.util import parse_url
8+
9+
from .._constants import SDK_VERSION
10+
from .._enums import FeatureUsageFlag, NationalClouds
11+
from .request_context import GraphRequestContext
12+
13+
14+
class GraphRequest(httpx.Request):
15+
context: GraphRequestContext
16+
17+
18+
class GraphTelemetryHandler(BaseMiddleware):
19+
"""Middleware component that attaches metadata to a Graph request in order to help
20+
the SDK team improve the developer experience.
21+
"""
22+
23+
async def send(self, request: GraphRequest, transport: AsyncKiotaTransport):
24+
"""Adds telemetry headers and sends the http request.
25+
"""
26+
self.set_request_context_and_feature_usage(request, transport)
27+
28+
if self.is_graph_url(request.url):
29+
self._add_client_request_id_header(request)
30+
self._append_sdk_version_header(request)
31+
self._add_host_os_header(request)
32+
self._add_runtime_environment_header(request)
33+
34+
response = await super().send(request, transport)
35+
return response
36+
37+
def set_request_context_and_feature_usage(
38+
self, request: GraphRequest, transport: AsyncKiotaTransport
39+
) -> GraphRequest:
40+
41+
request_options = {}
42+
options = request.headers.pop('request_options', None)
43+
if options:
44+
request_options = json.loads(options)
45+
46+
request.context = GraphRequestContext(request_options, request.headers)
47+
middleware = transport.pipeline._first_middleware
48+
while middleware:
49+
if isinstance(middleware, RedirectHandler):
50+
request.context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED
51+
if isinstance(middleware, RetryHandler):
52+
request.context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED
53+
54+
middleware = middleware.next
55+
56+
return request
57+
58+
def is_graph_url(self, url):
59+
"""Check if the request is made to a graph endpoint. We do not add telemetry headers to
60+
non-graph endpoints"""
61+
endpoints = set(item.value for item in NationalClouds)
62+
63+
base_url = parse_url(str(url))
64+
endpoint = f"{base_url.scheme}://{base_url.netloc}"
65+
return endpoint in endpoints
66+
67+
def _add_client_request_id_header(self, request) -> None:
68+
"""Add a client-request-id header with GUID value to request"""
69+
request.headers.update({'client-request-id': f'{request.context.client_request_id}'})
70+
71+
def _append_sdk_version_header(self, request) -> None:
72+
"""Add SdkVersion request header to each request to identify the language and
73+
version of the client SDK library(s).
74+
Also adds the featureUsage value.
75+
"""
76+
if 'sdkVersion' in request.headers:
77+
sdk_version = request.headers.get('sdkVersion')
78+
if not sdk_version == f'graph-python-core/{SDK_VERSION} '\
79+
f'(featureUsage={request.context.feature_usage})':
80+
request.headers.update(
81+
{
82+
'sdkVersion':
83+
f'graph-python-core/{SDK_VERSION},{ sdk_version} '\
84+
f'(featureUsage={request.context.feature_usage})'
85+
}
86+
)
87+
else:
88+
request.headers.update(
89+
{
90+
'sdkVersion':
91+
f'graph-python-core/{SDK_VERSION} '\
92+
f'(featureUsage={request.context.feature_usage})'
93+
}
94+
)
95+
96+
def _add_host_os_header(self, request) -> None:
97+
"""
98+
Add HostOS request header to each request to help identify the OS
99+
on which our client SDK is running on
100+
"""
101+
system = platform.system()
102+
version = platform.version()
103+
host_os = f'{system} {version}'
104+
request.headers.update({'HostOs': host_os})
105+
106+
def _add_runtime_environment_header(self, request) -> None:
107+
"""
108+
Add RuntimeEnvironment request header to capture the runtime framework
109+
on which the client SDK is running on.
110+
"""
111+
python_version = platform.python_version()
112+
runtime_environment = f'Python/{python_version}'
113+
request.headers.update({'RuntimeEnvironment': runtime_environment})

tests/conftest.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import httpx
22
import pytest
33
from kiota_abstractions.authentication import AnonymousAuthenticationProvider
4-
from kiota_authentication_azure.azure_identity_access_token_provider import (
5-
AzureIdentityAccessTokenProvider,
6-
)
74

8-
from msgraph.core import APIVersion, NationalClouds
9-
from msgraph.core.graph_client_factory import GraphClientFactory
10-
from msgraph.core.middleware import GraphRequestContext
5+
from msgraph_core import APIVersion, NationalClouds
6+
from msgraph_core.graph_client_factory import GraphClientFactory
7+
from msgraph_core.middleware import GraphRequestContext
118

129
BASE_URL = NationalClouds.Global + '/' + APIVersion.v1
1310

tests/unit/test_base_graph_request_adapter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import httpx
22
import pytest
3-
from asyncmock import AsyncMock
43
from kiota_abstractions.serialization import (
54
ParseNodeFactoryRegistry,
65
SerializationWriterFactoryRegistry,
76
)
87

9-
from msgraph.core.base_graph_request_adapter import BaseGraphRequestAdapter
8+
from msgraph_core.base_graph_request_adapter import BaseGraphRequestAdapter
109

1110

1211
def test_create_graph_request_adapter(mock_auth_provider):

0 commit comments

Comments
 (0)