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.
Files changed (34) hide show
  1. graphplug-0.2.0/LICENSE +21 -0
  2. graphplug-0.2.0/PKG-INFO +234 -0
  3. graphplug-0.2.0/README.md +205 -0
  4. graphplug-0.2.0/graphplug/__init__.py +378 -0
  5. graphplug-0.2.0/graphplug/_auth.py +390 -0
  6. graphplug-0.2.0/graphplug/_errors.py +176 -0
  7. graphplug-0.2.0/graphplug/_http.py +198 -0
  8. graphplug-0.2.0/graphplug/_log.py +104 -0
  9. graphplug-0.2.0/graphplug/_operations.py +310 -0
  10. graphplug-0.2.0/graphplug/_request.py +135 -0
  11. graphplug-0.2.0/graphplug/_resources/__init__.py +10 -0
  12. graphplug-0.2.0/graphplug/_resources/base.py +70 -0
  13. graphplug-0.2.0/graphplug/_resources/calendar.py +194 -0
  14. graphplug-0.2.0/graphplug/_resources/files.py +149 -0
  15. graphplug-0.2.0/graphplug/_resources/mail.py +196 -0
  16. graphplug-0.2.0/graphplug/_resources/teams.py +146 -0
  17. graphplug-0.2.0/graphplug/_resources/users.py +121 -0
  18. graphplug-0.2.0/graphplug/_scopes.py +56 -0
  19. graphplug-0.2.0/graphplug/py.typed +0 -0
  20. graphplug-0.2.0/graphplug.egg-info/PKG-INFO +234 -0
  21. graphplug-0.2.0/graphplug.egg-info/SOURCES.txt +32 -0
  22. graphplug-0.2.0/graphplug.egg-info/dependency_links.txt +1 -0
  23. graphplug-0.2.0/graphplug.egg-info/requires.txt +3 -0
  24. graphplug-0.2.0/graphplug.egg-info/top_level.txt +1 -0
  25. graphplug-0.2.0/pyproject.toml +53 -0
  26. graphplug-0.2.0/setup.cfg +4 -0
  27. graphplug-0.2.0/tests/test_errors_and_logging.py +210 -0
  28. graphplug-0.2.0/tests/test_files_teams_users.py +409 -0
  29. graphplug-0.2.0/tests/test_operations.py +324 -0
  30. graphplug-0.2.0/tests/test_redirect_and_construction.py +398 -0
  31. graphplug-0.2.0/tests/test_requests.py +255 -0
  32. graphplug-0.2.0/tests/test_resources.py +297 -0
  33. graphplug-0.2.0/tests/test_signin.py +247 -0
  34. graphplug-0.2.0/tests/test_transport.py +181 -0
@@ -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.
@@ -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.