contextbase-plugin-microsoft-mail 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from asyncio import AbstractEventLoop
5
+ from collections.abc import Awaitable, Mapping
6
+ from dataclasses import make_dataclass
7
+ from typing import Any
8
+ from urllib.parse import quote
9
+
10
+ import httpx
11
+ from azure.core.credentials_async import AsyncTokenCredential
12
+ from azure.identity.aio import ClientSecretCredential
13
+ from kiota_abstractions.base_request_configuration import RequestConfiguration
14
+ from kiota_abstractions.headers_collection import HeadersCollection
15
+ from kiota_abstractions.serialization import Parsable
16
+ from kiota_authentication_azure.azure_identity_authentication_provider import (
17
+ AzureIdentityAuthenticationProvider,
18
+ )
19
+ from kiota_serialization_json.json_serialization_writer import JsonSerializationWriter
20
+ from msgraph import GraphServiceClient
21
+ from msgraph.graph_request_adapter import (
22
+ GraphRequestAdapter,
23
+ options as _GRAPH_DEFAULT_OPTIONS,
24
+ )
25
+ from msgraph_core import APIVersion, GraphClientFactory
26
+ from shared_types.client_credentials_auth import ClientCredentialsAuth
27
+
28
+ _GRAPH_SCOPES = ["https://graph.microsoft.com/.default"]
29
+
30
+
31
+ def _build_h11_request_adapter(credential: AsyncTokenCredential) -> GraphRequestAdapter:
32
+ """Build a `GraphRequestAdapter` whose underlying httpx transport is
33
+ HTTP/1.1 only.
34
+
35
+ Force HTTP/1.1 (kiota's default is `http2=True`). Microsoft Graph's
36
+ frontdoor terminates HTTP/2 connections at ~30s with an advisory
37
+ GOAWAY (`error_code=NO_ERROR`, `last_stream_id=2^31-1`; RFC 7540
38
+ §6.8 graceful shutdown). httpcore turns that into a fatal
39
+ `RemoteProtocolError` for the in-flight stream
40
+ (`httpcore/_async/http2.py:350-355` — `stream_id > last_stream_id`
41
+ is unreachable when `last_stream_id` is `HIGHEST_ALLOWED_STREAM_ID`),
42
+ and kiota_http's `RetryHandler` doesn't catch transport exceptions
43
+ (it retries on status codes 429/503/504 only). HTTP/1.1 keep-alive
44
+ has no equivalent server-driven termination — empirically, a single
45
+ TCP connection comfortably serves 600+ requests over ~40s with the
46
+ same per-request latency as HTTP/2. Full empirical breakdown at
47
+ `internal/in-progress/2026-05-14-codecreators-komal-first-sync.md`
48
+ (B4 entry).
49
+ """
50
+ raw = httpx.AsyncClient(
51
+ http2=False,
52
+ base_url=f"https://graph.microsoft.com/{APIVersion.v1.value}",
53
+ )
54
+ wrapped = GraphClientFactory.create_with_default_middleware(
55
+ api_version=APIVersion.v1,
56
+ client=raw,
57
+ options=_GRAPH_DEFAULT_OPTIONS,
58
+ )
59
+ auth_provider = AzureIdentityAuthenticationProvider(
60
+ credential, scopes=_GRAPH_SCOPES
61
+ )
62
+ return GraphRequestAdapter(auth_provider, client=wrapped)
63
+
64
+
65
+ def _query_parameters_from_mapping(
66
+ query_params: Mapping[str, object | None] | None,
67
+ ) -> object | None:
68
+ values = [
69
+ (f"param_{index}", name, value)
70
+ for index, (name, value) in enumerate((query_params or {}).items())
71
+ if value is not None
72
+ ]
73
+ if not values:
74
+ return None
75
+
76
+ encoded_name_by_field = {
77
+ field_name: quote(name, safe="") for field_name, name, _value in values
78
+ }
79
+
80
+ def get_query_parameter(_self: object, original_name: str) -> str:
81
+ return encoded_name_by_field.get(original_name, original_name)
82
+
83
+ query_type = make_dataclass(
84
+ "_GraphQueryParameters",
85
+ [(field_name, object) for field_name, _name, _value in values],
86
+ namespace={"get_query_parameter": get_query_parameter},
87
+ frozen=True,
88
+ )
89
+ return query_type(
90
+ **{field_name: value for field_name, _name, value in values},
91
+ )
92
+
93
+
94
+ def _request_configuration(
95
+ *,
96
+ query_params: Mapping[str, object | None] | None = None,
97
+ prefer_header: str | None = None,
98
+ ) -> RequestConfiguration[Any]:
99
+ cfg: RequestConfiguration[Any] = RequestConfiguration()
100
+ cfg.headers = HeadersCollection()
101
+ cfg.query_parameters = _query_parameters_from_mapping(query_params)
102
+ if prefer_header:
103
+ cfg.headers.add("Prefer", prefer_header)
104
+ return cfg
105
+
106
+
107
+ class SyncGraphMailClient:
108
+ def __init__(
109
+ self,
110
+ *,
111
+ auth: ClientCredentialsAuth,
112
+ tenant_id: str,
113
+ mailbox_user_id: str,
114
+ ):
115
+ self._auth = auth
116
+ self._tenant_id = tenant_id
117
+ self._mailbox_user_id = mailbox_user_id
118
+ self._loop: AbstractEventLoop | None = None
119
+ self._credential: ClientSecretCredential | None = None
120
+ self._client: GraphServiceClient | None = None
121
+
122
+ def __enter__(self) -> SyncGraphMailClient:
123
+ self._loop = asyncio.new_event_loop()
124
+ self._loop.run_until_complete(self._open())
125
+ return self
126
+
127
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
128
+ assert self._loop is not None
129
+ self._loop.run_until_complete(self._close())
130
+ self._loop.close()
131
+
132
+ async def _open(self) -> None:
133
+ self._credential = ClientSecretCredential(
134
+ tenant_id=self._tenant_id,
135
+ client_id=self._auth.client_id,
136
+ client_secret=self._auth.client_secret,
137
+ )
138
+ self._client = GraphServiceClient(
139
+ request_adapter=_build_h11_request_adapter(self._credential),
140
+ )
141
+
142
+ async def _close(self) -> None:
143
+ if self._credential is not None:
144
+ await self._credential.close()
145
+
146
+ def _run(self, request: Awaitable[Any]) -> Any:
147
+ assert self._loop is not None
148
+ return self._loop.run_until_complete(request)
149
+
150
+ @property
151
+ def mailbox_user_id(self) -> str:
152
+ return self._mailbox_user_id
153
+
154
+ def get_folder_delta_page(
155
+ self,
156
+ *,
157
+ delta_url: str | None,
158
+ query_params: Mapping[str, object | None],
159
+ prefer_header: str | None,
160
+ ) -> Any:
161
+ assert self._client is not None
162
+ builder = self._client.users.by_user_id(self.mailbox_user_id).mail_folders.delta
163
+ if delta_url:
164
+ builder = builder.with_url(delta_url)
165
+
166
+ return self._run(
167
+ builder.get(
168
+ request_configuration=_request_configuration(
169
+ query_params=None if delta_url else query_params,
170
+ prefer_header=prefer_header,
171
+ ),
172
+ )
173
+ )
174
+
175
+ def get_message_delta_page(
176
+ self,
177
+ *,
178
+ folder_id: str,
179
+ delta_url: str | None,
180
+ query_params: Mapping[str, object | None],
181
+ prefer_header: str | None,
182
+ ) -> Any:
183
+ assert self._client is not None
184
+ builder = (
185
+ self._client.users.by_user_id(self.mailbox_user_id)
186
+ .mail_folders.by_mail_folder_id(folder_id)
187
+ .messages.delta
188
+ )
189
+ if delta_url:
190
+ builder = builder.with_url(delta_url)
191
+
192
+ return self._run(
193
+ builder.get(
194
+ request_configuration=_request_configuration(
195
+ query_params=None if delta_url else query_params,
196
+ prefer_header=prefer_header,
197
+ ),
198
+ )
199
+ )
200
+
201
+ def get_attachment_full(
202
+ self,
203
+ *,
204
+ message_id: str,
205
+ attachment_id: str,
206
+ prefer_header: str | None,
207
+ ) -> Any:
208
+ """GET /messages/{m}/attachments/{a} — returns the full attachment object
209
+ including contentBytes (for fileAttachment), contentId, contentLocation."""
210
+ assert self._client is not None
211
+ builder = (
212
+ self._client.users.by_user_id(self.mailbox_user_id)
213
+ .messages.by_message_id(message_id)
214
+ .attachments.by_attachment_id(attachment_id)
215
+ )
216
+ return self._run(
217
+ builder.get(
218
+ request_configuration=_request_configuration(
219
+ prefer_header=prefer_header,
220
+ ),
221
+ ),
222
+ )
223
+
224
+
225
+ def graph_object_to_payload(obj: object) -> dict[str, Any]:
226
+ if isinstance(obj, Mapping):
227
+ return dict(obj)
228
+ if not isinstance(obj, Parsable):
229
+ raise TypeError(
230
+ "Expected a Microsoft Graph Kiota Parsable object or mapping; "
231
+ f"got {type(obj).__name__}."
232
+ )
233
+
234
+ writer = JsonSerializationWriter()
235
+ writer.write_object_value(None, obj)
236
+ payload = writer.value if writer.value is not None else writer.writer
237
+ if not isinstance(payload, dict):
238
+ raise RuntimeError(
239
+ "Microsoft Graph Kiota serializer returned a non-object payload."
240
+ )
241
+ raw = dict(payload)
242
+ additional_data = getattr(obj, "additional_data", None)
243
+ if isinstance(additional_data, dict):
244
+ raw["additional_data"] = dict(additional_data)
245
+ return raw