graphplug 0.2.0__tar.gz
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.
- graphplug-0.2.0/LICENSE +21 -0
- graphplug-0.2.0/PKG-INFO +234 -0
- graphplug-0.2.0/README.md +205 -0
- graphplug-0.2.0/graphplug/__init__.py +378 -0
- graphplug-0.2.0/graphplug/_auth.py +390 -0
- graphplug-0.2.0/graphplug/_errors.py +176 -0
- graphplug-0.2.0/graphplug/_http.py +198 -0
- graphplug-0.2.0/graphplug/_log.py +104 -0
- graphplug-0.2.0/graphplug/_operations.py +310 -0
- graphplug-0.2.0/graphplug/_request.py +135 -0
- graphplug-0.2.0/graphplug/_resources/__init__.py +10 -0
- graphplug-0.2.0/graphplug/_resources/base.py +70 -0
- graphplug-0.2.0/graphplug/_resources/calendar.py +194 -0
- graphplug-0.2.0/graphplug/_resources/files.py +149 -0
- graphplug-0.2.0/graphplug/_resources/mail.py +196 -0
- graphplug-0.2.0/graphplug/_resources/teams.py +146 -0
- graphplug-0.2.0/graphplug/_resources/users.py +121 -0
- graphplug-0.2.0/graphplug/_scopes.py +56 -0
- graphplug-0.2.0/graphplug/py.typed +0 -0
- graphplug-0.2.0/graphplug.egg-info/PKG-INFO +234 -0
- graphplug-0.2.0/graphplug.egg-info/SOURCES.txt +32 -0
- graphplug-0.2.0/graphplug.egg-info/dependency_links.txt +1 -0
- graphplug-0.2.0/graphplug.egg-info/requires.txt +3 -0
- graphplug-0.2.0/graphplug.egg-info/top_level.txt +1 -0
- graphplug-0.2.0/pyproject.toml +53 -0
- graphplug-0.2.0/setup.cfg +4 -0
- graphplug-0.2.0/tests/test_errors_and_logging.py +210 -0
- graphplug-0.2.0/tests/test_files_teams_users.py +409 -0
- graphplug-0.2.0/tests/test_operations.py +324 -0
- graphplug-0.2.0/tests/test_redirect_and_construction.py +398 -0
- graphplug-0.2.0/tests/test_requests.py +255 -0
- graphplug-0.2.0/tests/test_resources.py +297 -0
- graphplug-0.2.0/tests/test_signin.py +247 -0
- graphplug-0.2.0/tests/test_transport.py +181 -0
graphplug-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VikramBalaji1
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
graphplug-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graphplug
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Plug-and-play Graph API for Python: mail, calendar, files, teams, users and generic requests.
|
|
5
|
+
Author: Arvind Vikram
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: microsoft-graph,entra-id,azure-ad,outlook,onedrive,teams,asyncio
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Communications :: Email
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: azure-identity>=1.19
|
|
26
|
+
Requires-Dist: msgraph-core>=1.3
|
|
27
|
+
Requires-Dist: msal>=1.31
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# graphplug
|
|
31
|
+
|
|
32
|
+
Plug-and-play Microsoft Graph for Python. Sending a mail is one call. Booking a Teams meeting is
|
|
33
|
+
one call. Authentication, retries, throttling, paging, batching and large file transfers happen
|
|
34
|
+
underneath.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
from graphplug import GraphClient, Scopes
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
graph = await GraphClient.device_code(TENANT, CLIENT, Scopes.MAIL_SEND)
|
|
42
|
+
|
|
43
|
+
async with graph:
|
|
44
|
+
await graph.mail.send(
|
|
45
|
+
to="alice@contoso.com",
|
|
46
|
+
subject="Quarterly report",
|
|
47
|
+
body="<p>Attached.</p>", html=True,
|
|
48
|
+
attachments=["report.pdf"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **Start here:** [USAGE.md](USAGE.md) is the full walkthrough. When something fails, see
|
|
55
|
+
> [docs/troubleshooting.md](docs/troubleshooting.md). For working code, see [samples/](samples).
|
|
56
|
+
> [ARCHITECTURE.md](ARCHITECTURE.md) explains *why* the rules are what they are.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install graphplug
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Two dependencies, both Microsoft's own — `azure-identity` for credentials, `msgraph-core` for the
|
|
67
|
+
supported middleware pipeline. Pure Python, so it installs anywhere.
|
|
68
|
+
|
|
69
|
+
`msgraph-sdk` is deliberately not used: its dependency tree does not resolve in practice, hanging
|
|
70
|
+
`pip` and `uv` indefinitely. `msgraph-core` resolves in a few seconds and carries the parts that
|
|
71
|
+
matter.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## What you get
|
|
76
|
+
|
|
77
|
+
**A resource layer, over five areas.** Graph's `sendMail` payload is roughly twenty lines of
|
|
78
|
+
nested JSON — recipients as objects inside objects, a body with a content type, attachments
|
|
79
|
+
base64-encoded with an `@odata.type` discriminator. A Teams meeting needs `isOnlineMeeting` *and*
|
|
80
|
+
`onlineMeetingProvider`. A drive item is `/me/drive/root:/reports/q3.xlsx:` — with a closing colon
|
|
81
|
+
everybody forgets. A user search returns a bare 400 without a `ConsistencyLevel` header. All of
|
|
82
|
+
that is built for you.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
await graph.mail.send(to=..., subject=..., body=..., attachments=[...])
|
|
86
|
+
async for message in graph.mail.inbox(unread_only=True): ...
|
|
87
|
+
|
|
88
|
+
event = await graph.calendar.schedule(subject=..., start=..., end=..., online=True)
|
|
89
|
+
print(event["onlineMeeting"]["joinUrl"])
|
|
90
|
+
|
|
91
|
+
await graph.files.upload("q3.xlsx", to="/reports/2026/q3.xlsx")
|
|
92
|
+
url = await graph.files.share_link("/reports/2026/q3.xlsx", kind="edit")
|
|
93
|
+
|
|
94
|
+
channel = await graph.teams.channel_by_name(team_id, "deploys")
|
|
95
|
+
await graph.teams.post(team_id, channel["id"], f"Report is up: {url}")
|
|
96
|
+
|
|
97
|
+
async for person in graph.users.find("smith"): ...
|
|
98
|
+
boss = await graph.users.manager()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Everything generic, too.** `get`, `post`, `patch`, `delete`, `paged`, `batch`, `download`,
|
|
102
|
+
`upload` — every `v1.0` and `beta` endpoint reachable without waiting for a typed wrapper.
|
|
103
|
+
|
|
104
|
+
**Speed that does not need orchestrating.** Batching is the lever, not asyncio:
|
|
105
|
+
|
|
106
|
+
| | 500 user lookups |
|
|
107
|
+
|---|---|
|
|
108
|
+
| One at a time | 500 round-trips |
|
|
109
|
+
| `graph.batch(...)` | **25 round-trips** |
|
|
110
|
+
| …dispatched concurrently | **~5 round-trip times** |
|
|
111
|
+
|
|
112
|
+
`batch`, `get_many` and `send_many` chunk at Graph's limit of 20 and dispatch under a bounded
|
|
113
|
+
semaphore. You never write `asyncio.gather`, and you do not get throttled for going too wide.
|
|
114
|
+
|
|
115
|
+
**Adding a resource is one subclass.** `list`, `get`, `create`, `update`, `delete` and `get_many`
|
|
116
|
+
come from a shared base; a new resource sets a path and adds whatever is specific to it. The five
|
|
117
|
+
that ship are each about 150 lines and are worth reading as worked examples.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## The two access models
|
|
122
|
+
|
|
123
|
+
Choosing wrong is how a script ends up with far more reach than intended.
|
|
124
|
+
|
|
125
|
+
| | **Application-level** | **Delegated** |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Acting as | The application itself | A signed-in person |
|
|
128
|
+
| Reach | **The whole tenant** | Only what that person can already do |
|
|
129
|
+
| `graph.mail` / `graph.calendar` | No — there is no user | Yes |
|
|
130
|
+
| Human needed | No | Yes, at first sign-in |
|
|
131
|
+
| Constructor | `app_only`, `from_env` | `device_code`, `interactive` |
|
|
132
|
+
|
|
133
|
+
`Mail.Read` as an **application** permission reads every mailbox in the tenant. The same name as a
|
|
134
|
+
**delegated** permission reads only the signed-in person's mail.
|
|
135
|
+
|
|
136
|
+
Supported sign-ins: client secret, device code, and authorization code with PKCE. Certificate,
|
|
137
|
+
managed identity and on-behalf-of go through `GraphClient.from_credential(...)`, which takes any
|
|
138
|
+
`azure-identity` credential — sync or async — and is the reason those flows need no support here.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Security properties
|
|
143
|
+
|
|
144
|
+
Deliberate, and tested rather than documented and hoped for.
|
|
145
|
+
|
|
146
|
+
- **Delegated scopes are never defaulted.** `.default` on a delegated flow silently requests every
|
|
147
|
+
scope ever consented for that client. Omitting scopes is an error naming the field.
|
|
148
|
+
- **A caller-supplied `Authorization` header is rejected** before the request leaves.
|
|
149
|
+
- **Response headers pass an allowlist, never a denylist.** A denylist fails open on whatever
|
|
150
|
+
header Microsoft adds tomorrow.
|
|
151
|
+
- **The PKCE verifier never leaves the process**, and `state` is validated internally so the CSRF
|
|
152
|
+
check cannot be skipped.
|
|
153
|
+
- **The bearer token is withheld from any host but `graph.microsoft.com`.** Pre-authenticated
|
|
154
|
+
download URLs still work; they simply travel unauthenticated.
|
|
155
|
+
- **Nothing is written to disk.** The token cache is in memory for the life of the client.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Errors
|
|
160
|
+
|
|
161
|
+
One exception type carrying data, rather than a hierarchy.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
except GraphError as e:
|
|
165
|
+
e.status # HTTP status, or 0 when there was no response at all
|
|
166
|
+
e.code # "itemNotFound", or a core code such as "consentRequired"
|
|
167
|
+
e.message
|
|
168
|
+
e.request_id # quote this to Microsoft support
|
|
169
|
+
e.retry_after
|
|
170
|
+
e.inner # Graph's own inner error, verbatim
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Throttling is handled for you — the pipeline honours `Retry-After`. Catching
|
|
174
|
+
`activityLimitReached` means the retry budget ran out, and `retry_after` tells you how long to
|
|
175
|
+
wait. Every code and its fix is in [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Logging
|
|
180
|
+
|
|
181
|
+
Off unless asked. `GRAPHPLUG_LOG_LEVEL=info` or `=error`; one JSON object per line on stderr.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
{"level":"info","event":"request","method":"GET","url":"https://graph.microsoft.com/v1.0/users","status":200,"ms":214,"requestId":"a1b2c3d4","errorCode":null}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
URLs are logged **without their query string**, because an OData `$filter` routinely carries email
|
|
188
|
+
addresses. Headers, bodies and credential material are never logged.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
pip install -e .
|
|
196
|
+
python -m unittest discover -s tests # 188 tests
|
|
197
|
+
python -m build --wheel
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
No container, no compiler, no platform-specific build.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Status
|
|
205
|
+
|
|
206
|
+
The package is complete and tested. **188 tests at 94% line coverage**, covering the middleware
|
|
207
|
+
contract, request construction, paging, batching, file round-trips, the exact paths and payloads
|
|
208
|
+
all five resources build, the error taxonomy, concurrency bounds, the sign-in orchestration, the
|
|
209
|
+
loopback redirect listener, the drive addressing rules and the logger.
|
|
210
|
+
|
|
211
|
+
### What still needs a tenant
|
|
212
|
+
|
|
213
|
+
Everything above is verified without one. These cannot be:
|
|
214
|
+
|
|
215
|
+
- Whether each sign-in flow completes against real Entra.
|
|
216
|
+
- Whether the permissions each resource declares are sufficient in practice.
|
|
217
|
+
|
|
218
|
+
Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID` and `AZURE_CLIENT_SECRET` and the live checks become
|
|
219
|
+
available.
|
|
220
|
+
|
|
221
|
+
### History
|
|
222
|
+
|
|
223
|
+
An earlier version of this package was a C# core compiled to a native library and reached through
|
|
224
|
+
a C ABI. It has been removed. Most of the rules here — the header allowlist, the error taxonomy, the
|
|
225
|
+
upload thresholds, the batch chunking, the two access models — were worked out there and survived
|
|
226
|
+
the rewrite unchanged, which is decent evidence they were about Graph rather than about C#.
|
|
227
|
+
[ARCHITECTURE.md](ARCHITECTURE.md) documents the current design and records the reasoning.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Licence
|
|
232
|
+
|
|
233
|
+
[MIT](LICENSE). `azure-identity` and `msgraph-core` are MIT too, so nothing here carries an
|
|
234
|
+
obligation you did not choose.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# graphplug
|
|
2
|
+
|
|
3
|
+
Plug-and-play Microsoft Graph for Python. Sending a mail is one call. Booking a Teams meeting is
|
|
4
|
+
one call. Authentication, retries, throttling, paging, batching and large file transfers happen
|
|
5
|
+
underneath.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import asyncio
|
|
9
|
+
from graphplug import GraphClient, Scopes
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
graph = await GraphClient.device_code(TENANT, CLIENT, Scopes.MAIL_SEND)
|
|
13
|
+
|
|
14
|
+
async with graph:
|
|
15
|
+
await graph.mail.send(
|
|
16
|
+
to="alice@contoso.com",
|
|
17
|
+
subject="Quarterly report",
|
|
18
|
+
body="<p>Attached.</p>", html=True,
|
|
19
|
+
attachments=["report.pdf"],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
asyncio.run(main())
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> **Start here:** [USAGE.md](USAGE.md) is the full walkthrough. When something fails, see
|
|
26
|
+
> [docs/troubleshooting.md](docs/troubleshooting.md). For working code, see [samples/](samples).
|
|
27
|
+
> [ARCHITECTURE.md](ARCHITECTURE.md) explains *why* the rules are what they are.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install graphplug
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Two dependencies, both Microsoft's own — `azure-identity` for credentials, `msgraph-core` for the
|
|
38
|
+
supported middleware pipeline. Pure Python, so it installs anywhere.
|
|
39
|
+
|
|
40
|
+
`msgraph-sdk` is deliberately not used: its dependency tree does not resolve in practice, hanging
|
|
41
|
+
`pip` and `uv` indefinitely. `msgraph-core` resolves in a few seconds and carries the parts that
|
|
42
|
+
matter.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## What you get
|
|
47
|
+
|
|
48
|
+
**A resource layer, over five areas.** Graph's `sendMail` payload is roughly twenty lines of
|
|
49
|
+
nested JSON — recipients as objects inside objects, a body with a content type, attachments
|
|
50
|
+
base64-encoded with an `@odata.type` discriminator. A Teams meeting needs `isOnlineMeeting` *and*
|
|
51
|
+
`onlineMeetingProvider`. A drive item is `/me/drive/root:/reports/q3.xlsx:` — with a closing colon
|
|
52
|
+
everybody forgets. A user search returns a bare 400 without a `ConsistencyLevel` header. All of
|
|
53
|
+
that is built for you.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
await graph.mail.send(to=..., subject=..., body=..., attachments=[...])
|
|
57
|
+
async for message in graph.mail.inbox(unread_only=True): ...
|
|
58
|
+
|
|
59
|
+
event = await graph.calendar.schedule(subject=..., start=..., end=..., online=True)
|
|
60
|
+
print(event["onlineMeeting"]["joinUrl"])
|
|
61
|
+
|
|
62
|
+
await graph.files.upload("q3.xlsx", to="/reports/2026/q3.xlsx")
|
|
63
|
+
url = await graph.files.share_link("/reports/2026/q3.xlsx", kind="edit")
|
|
64
|
+
|
|
65
|
+
channel = await graph.teams.channel_by_name(team_id, "deploys")
|
|
66
|
+
await graph.teams.post(team_id, channel["id"], f"Report is up: {url}")
|
|
67
|
+
|
|
68
|
+
async for person in graph.users.find("smith"): ...
|
|
69
|
+
boss = await graph.users.manager()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Everything generic, too.** `get`, `post`, `patch`, `delete`, `paged`, `batch`, `download`,
|
|
73
|
+
`upload` — every `v1.0` and `beta` endpoint reachable without waiting for a typed wrapper.
|
|
74
|
+
|
|
75
|
+
**Speed that does not need orchestrating.** Batching is the lever, not asyncio:
|
|
76
|
+
|
|
77
|
+
| | 500 user lookups |
|
|
78
|
+
|---|---|
|
|
79
|
+
| One at a time | 500 round-trips |
|
|
80
|
+
| `graph.batch(...)` | **25 round-trips** |
|
|
81
|
+
| …dispatched concurrently | **~5 round-trip times** |
|
|
82
|
+
|
|
83
|
+
`batch`, `get_many` and `send_many` chunk at Graph's limit of 20 and dispatch under a bounded
|
|
84
|
+
semaphore. You never write `asyncio.gather`, and you do not get throttled for going too wide.
|
|
85
|
+
|
|
86
|
+
**Adding a resource is one subclass.** `list`, `get`, `create`, `update`, `delete` and `get_many`
|
|
87
|
+
come from a shared base; a new resource sets a path and adds whatever is specific to it. The five
|
|
88
|
+
that ship are each about 150 lines and are worth reading as worked examples.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## The two access models
|
|
93
|
+
|
|
94
|
+
Choosing wrong is how a script ends up with far more reach than intended.
|
|
95
|
+
|
|
96
|
+
| | **Application-level** | **Delegated** |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Acting as | The application itself | A signed-in person |
|
|
99
|
+
| Reach | **The whole tenant** | Only what that person can already do |
|
|
100
|
+
| `graph.mail` / `graph.calendar` | No — there is no user | Yes |
|
|
101
|
+
| Human needed | No | Yes, at first sign-in |
|
|
102
|
+
| Constructor | `app_only`, `from_env` | `device_code`, `interactive` |
|
|
103
|
+
|
|
104
|
+
`Mail.Read` as an **application** permission reads every mailbox in the tenant. The same name as a
|
|
105
|
+
**delegated** permission reads only the signed-in person's mail.
|
|
106
|
+
|
|
107
|
+
Supported sign-ins: client secret, device code, and authorization code with PKCE. Certificate,
|
|
108
|
+
managed identity and on-behalf-of go through `GraphClient.from_credential(...)`, which takes any
|
|
109
|
+
`azure-identity` credential — sync or async — and is the reason those flows need no support here.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Security properties
|
|
114
|
+
|
|
115
|
+
Deliberate, and tested rather than documented and hoped for.
|
|
116
|
+
|
|
117
|
+
- **Delegated scopes are never defaulted.** `.default` on a delegated flow silently requests every
|
|
118
|
+
scope ever consented for that client. Omitting scopes is an error naming the field.
|
|
119
|
+
- **A caller-supplied `Authorization` header is rejected** before the request leaves.
|
|
120
|
+
- **Response headers pass an allowlist, never a denylist.** A denylist fails open on whatever
|
|
121
|
+
header Microsoft adds tomorrow.
|
|
122
|
+
- **The PKCE verifier never leaves the process**, and `state` is validated internally so the CSRF
|
|
123
|
+
check cannot be skipped.
|
|
124
|
+
- **The bearer token is withheld from any host but `graph.microsoft.com`.** Pre-authenticated
|
|
125
|
+
download URLs still work; they simply travel unauthenticated.
|
|
126
|
+
- **Nothing is written to disk.** The token cache is in memory for the life of the client.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Errors
|
|
131
|
+
|
|
132
|
+
One exception type carrying data, rather than a hierarchy.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
except GraphError as e:
|
|
136
|
+
e.status # HTTP status, or 0 when there was no response at all
|
|
137
|
+
e.code # "itemNotFound", or a core code such as "consentRequired"
|
|
138
|
+
e.message
|
|
139
|
+
e.request_id # quote this to Microsoft support
|
|
140
|
+
e.retry_after
|
|
141
|
+
e.inner # Graph's own inner error, verbatim
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Throttling is handled for you — the pipeline honours `Retry-After`. Catching
|
|
145
|
+
`activityLimitReached` means the retry budget ran out, and `retry_after` tells you how long to
|
|
146
|
+
wait. Every code and its fix is in [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Logging
|
|
151
|
+
|
|
152
|
+
Off unless asked. `GRAPHPLUG_LOG_LEVEL=info` or `=error`; one JSON object per line on stderr.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
{"level":"info","event":"request","method":"GET","url":"https://graph.microsoft.com/v1.0/users","status":200,"ms":214,"requestId":"a1b2c3d4","errorCode":null}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
URLs are logged **without their query string**, because an OData `$filter` routinely carries email
|
|
159
|
+
addresses. Headers, bodies and credential material are never logged.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pip install -e .
|
|
167
|
+
python -m unittest discover -s tests # 188 tests
|
|
168
|
+
python -m build --wheel
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
No container, no compiler, no platform-specific build.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Status
|
|
176
|
+
|
|
177
|
+
The package is complete and tested. **188 tests at 94% line coverage**, covering the middleware
|
|
178
|
+
contract, request construction, paging, batching, file round-trips, the exact paths and payloads
|
|
179
|
+
all five resources build, the error taxonomy, concurrency bounds, the sign-in orchestration, the
|
|
180
|
+
loopback redirect listener, the drive addressing rules and the logger.
|
|
181
|
+
|
|
182
|
+
### What still needs a tenant
|
|
183
|
+
|
|
184
|
+
Everything above is verified without one. These cannot be:
|
|
185
|
+
|
|
186
|
+
- Whether each sign-in flow completes against real Entra.
|
|
187
|
+
- Whether the permissions each resource declares are sufficient in practice.
|
|
188
|
+
|
|
189
|
+
Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID` and `AZURE_CLIENT_SECRET` and the live checks become
|
|
190
|
+
available.
|
|
191
|
+
|
|
192
|
+
### History
|
|
193
|
+
|
|
194
|
+
An earlier version of this package was a C# core compiled to a native library and reached through
|
|
195
|
+
a C ABI. It has been removed. Most of the rules here — the header allowlist, the error taxonomy, the
|
|
196
|
+
upload thresholds, the batch chunking, the two access models — were worked out there and survived
|
|
197
|
+
the rewrite unchanged, which is decent evidence they were about Graph rather than about C#.
|
|
198
|
+
[ARCHITECTURE.md](ARCHITECTURE.md) documents the current design and records the reasoning.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Licence
|
|
203
|
+
|
|
204
|
+
[MIT](LICENSE). `azure-identity` and `msgraph-core` are MIT too, so nothing here carries an
|
|
205
|
+
obligation you did not choose.
|