graphplug 0.2.0__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,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,21 @@
1
+ graphplug/__init__.py,sha256=r2ZK2U3sckVVTWJHxVMhIU-SQcCKPdowneHlcqeiGa8,15483
2
+ graphplug/_auth.py,sha256=HLRQo1ZTYaqDGpr009l5-bZqm3fqsbEY6IDDhM3qhDg,15068
3
+ graphplug/_errors.py,sha256=jlmpAAT5Msmv33E0GA6kBgRFn4O1nJp97aOH2f_TxiI,6352
4
+ graphplug/_http.py,sha256=pGv0N_qB_LvIp9z4Mva_pZllRJ2Wr8pIo6Cv3u_ouq8,7776
5
+ graphplug/_log.py,sha256=c63sn-tExJA9EkCUapBN3L4tYgAtimhUoQcLUifowAo,3101
6
+ graphplug/_operations.py,sha256=cW0N3IDv8qhh5QF6EUy6OEhRWQaBnlBTbLToBz8m42E,11880
7
+ graphplug/_request.py,sha256=j4hSyAHpPq0zKcvMOcYpsAW_AFJ674XGnJvqgtm28FI,4893
8
+ graphplug/_scopes.py,sha256=0NWz5vXaNZI0nYgq6wsAOahIGu6lhV8YV_qWAcoYNN0,2129
9
+ graphplug/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ graphplug/_resources/__init__.py,sha256=QV-7kBZTwiFSIiT-ssTcxBvDJAvHFPc5T_VTDZxPbDQ,314
11
+ graphplug/_resources/base.py,sha256=MLK3HXeTfCsKx79Xko89XfUuKUZNcAAjxlAN3MW_Tos,3428
12
+ graphplug/_resources/calendar.py,sha256=ZmBSSx7sJa9Y_w34vJOn6Q1cu0CUV2XHBa49ULsXobo,7836
13
+ graphplug/_resources/files.py,sha256=uhvsmvnt3ykGoUSEpd0u6U1mca4F651C6DItqgGHKAM,6698
14
+ graphplug/_resources/mail.py,sha256=iL9d1BV3uDNfozQl6_zdVGU6yyIM0VPHi6p71oKLluQ,8088
15
+ graphplug/_resources/teams.py,sha256=LYYLo57PdWAgBz1FCWJjLqLavjrVjoMc94bIrqN8hHU,5955
16
+ graphplug/_resources/users.py,sha256=2sA07ZTPZ9BwFbT9-U166RWEVRrXI3rIvrHeNGZ6-qw,5497
17
+ graphplug-0.2.0.dist-info/licenses/LICENSE,sha256=3yDD8dhpZPQ2-cPjMpqNWJUk27z9oH4f8LpP4QU4LGg,1070
18
+ graphplug-0.2.0.dist-info/METADATA,sha256=C1BmzwEvgTeq8qLPsWjUWHbXOXoZUMDusvPFlRjun-o,9010
19
+ graphplug-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ graphplug-0.2.0.dist-info/top_level.txt,sha256=LI_UCvxnrmuPNdaIKxMO92vUcffnhVbKrbsrj6wh-Jk,10
21
+ graphplug-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ graphplug