msgraphkit 0.1.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.
graphkit/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ graphkit -- a small, dependency-light toolkit for authenticating to the
3
+ Microsoft Graph API (app-only / client credentials flow) and making
4
+ simple REST calls against it.
5
+
6
+ from graphkit import get_token, GraphClient
7
+
8
+ token = get_token()
9
+ graph = GraphClient(token)
10
+ me = graph.get("/users/someone@example.com")
11
+ """
12
+
13
+ from .auth import get_token, GraphAuthError
14
+ from .client import GraphClient
15
+
16
+ __all__ = ["get_token", "GraphAuthError", "GraphClient"]
17
+ __version__ = "0.1.0"
graphkit/auth.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ graphkit.auth
3
+ =============
4
+ Authenticates to the Microsoft Graph API using the OAuth2 "client
5
+ credentials" flow -- also called app-only auth. This is the flow you
6
+ want when a script needs to act on a mailbox, Planner plan, SharePoint
7
+ site, etc. *without* a human signing in interactively (e.g. a scheduled
8
+ job, a background automation, a shared-mailbox integration).
9
+
10
+ No secrets live in this file or anywhere else in this package. All
11
+ three required values are read from environment variables at runtime:
12
+
13
+ GRAPH_TENANT_ID
14
+ GRAPH_CLIENT_ID
15
+ GRAPH_CLIENT_SECRET
16
+
17
+ See the README for how to create these by registering an app in
18
+ Azure AD (Entra ID), and .env.example for a template you can copy to
19
+ a real .env file (which you should never commit to version control).
20
+
21
+ Usage
22
+ -----
23
+ from graphkit import get_token
24
+
25
+ token = get_token()
26
+ """
27
+
28
+ import os
29
+ from typing import Optional
30
+
31
+ import msal
32
+
33
+ GRAPH_SCOPE = ["https://graph.microsoft.com/.default"]
34
+
35
+
36
+ class GraphAuthError(RuntimeError):
37
+ """Raised when a Graph API access token could not be acquired."""
38
+
39
+
40
+ def _require_env(name: str) -> str:
41
+ value = os.environ.get(name)
42
+ if not value:
43
+ raise GraphAuthError(
44
+ f"Missing required environment variable: {name}\n"
45
+ "Set it in your shell, or add it to a .env file in your "
46
+ "project folder (see .env.example) and load it with "
47
+ "python-dotenv before calling get_token()."
48
+ )
49
+ return value
50
+
51
+
52
+ def get_token(
53
+ tenant_id: Optional[str] = None,
54
+ client_id: Optional[str] = None,
55
+ client_secret: Optional[str] = None,
56
+ ) -> str:
57
+ """
58
+ Acquire an app-only Microsoft Graph access token.
59
+
60
+ If tenant_id / client_id / client_secret are not passed in
61
+ directly, they are read from the GRAPH_TENANT_ID, GRAPH_CLIENT_ID,
62
+ and GRAPH_CLIENT_SECRET environment variables.
63
+
64
+ MSAL keeps its own in-memory token cache for the lifetime of the
65
+ process and will reuse a still-valid token instead of calling
66
+ Azure again, so it's fine to call this once per script, or before
67
+ every API call, without worrying about hammering the token
68
+ endpoint.
69
+
70
+ Raises GraphAuthError if credentials are missing or invalid.
71
+ """
72
+ tenant_id = tenant_id or _require_env("GRAPH_TENANT_ID")
73
+ client_id = client_id or _require_env("GRAPH_CLIENT_ID")
74
+ client_secret = client_secret or _require_env("GRAPH_CLIENT_SECRET")
75
+
76
+ app = msal.ConfidentialClientApplication(
77
+ client_id,
78
+ authority=f"https://login.microsoftonline.com/{tenant_id}",
79
+ client_credential=client_secret,
80
+ )
81
+
82
+ result = app.acquire_token_for_client(scopes=GRAPH_SCOPE)
83
+
84
+ if "access_token" not in result:
85
+ raise GraphAuthError(
86
+ result.get("error_description", str(result))
87
+ )
88
+
89
+ return result["access_token"]
graphkit/client.py ADDED
@@ -0,0 +1,88 @@
1
+ """
2
+ graphkit.client
3
+ ================
4
+ A deliberately thin wrapper around the Microsoft Graph REST API
5
+ (v1.0). It knows nothing about Outlook, Planner, SharePoint, or any
6
+ other specific Graph resource -- it just handles the repetitive parts
7
+ (base URL, auth header, JSON) so you can call any Graph endpoint with
8
+ one line and build your own helper functions on top.
9
+
10
+ Usage
11
+ -----
12
+ from graphkit import get_token, GraphClient
13
+
14
+ token = get_token()
15
+ graph = GraphClient(token)
16
+
17
+ me = graph.get("/users/someone@example.com")
18
+
19
+ graph.post("/users/someone@example.com/messages", {
20
+ "subject": "Hello",
21
+ "body": {"contentType": "Text", "content": "Hi there"},
22
+ "toRecipients": [{"emailAddress": {"address": "other@example.com"}}],
23
+ })
24
+ """
25
+
26
+ from typing import Optional
27
+
28
+ import requests
29
+
30
+ GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0"
31
+
32
+
33
+ class GraphClient:
34
+ """
35
+ Thin HTTP client for the Microsoft Graph API.
36
+
37
+ Pass in a bearer token from graphkit.auth.get_token(). One
38
+ GraphClient per token -- if a token expires mid-run, create a new
39
+ client with a freshly acquired token.
40
+ """
41
+
42
+ def __init__(self, token: str, base_url: str = GRAPH_BASE_URL):
43
+ self.token = token
44
+ self.base_url = base_url
45
+
46
+ def _headers(self, extra: Optional[dict] = None) -> dict:
47
+ headers = {"Authorization": f"Bearer {self.token}"}
48
+ if extra:
49
+ headers.update(extra)
50
+ return headers
51
+
52
+ def get(self, path: str) -> dict:
53
+ """GET a Graph path (e.g. '/me' or '/users/x@y.com/messages') and
54
+ return the parsed JSON body. Raises on non-2xx responses."""
55
+ r = requests.get(f"{self.base_url}{path}", headers=self._headers())
56
+ r.raise_for_status()
57
+ return r.json()
58
+
59
+ def post(self, path: str, body: dict) -> requests.Response:
60
+ """POST a JSON body to a Graph path. Returns the raw response --
61
+ check response.status_code (Graph typically returns 201 for a
62
+ successful create) rather than assuming success."""
63
+ return requests.post(
64
+ f"{self.base_url}{path}",
65
+ headers=self._headers({"Content-Type": "application/json"}),
66
+ json=body,
67
+ )
68
+
69
+ def patch(self, path: str, body: dict) -> requests.Response:
70
+ """PATCH (partial update) a JSON body to a Graph path."""
71
+ return requests.patch(
72
+ f"{self.base_url}{path}",
73
+ headers=self._headers({"Content-Type": "application/json"}),
74
+ json=body,
75
+ )
76
+
77
+ def delete(self, path: str) -> requests.Response:
78
+ """DELETE a Graph resource."""
79
+ return requests.delete(f"{self.base_url}{path}", headers=self._headers())
80
+
81
+ @staticmethod
82
+ def error_message(response: requests.Response) -> str:
83
+ """Best-effort extraction of a human-readable error message from
84
+ a failed Graph response, for logging/printing."""
85
+ try:
86
+ return response.json().get("error", {}).get("message", response.text[:300])
87
+ except ValueError:
88
+ return response.text[:300]
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: msgraphkit
3
+ Version: 0.1.0
4
+ Summary: A small, dependency-light toolkit for authenticating to the Microsoft Graph API (app-only / client credentials flow) and making simple REST calls against it.
5
+ Author: Duckboard
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Duckboard/graphkit
8
+ Project-URL: Buy me a coffee, https://ko-fi.com/duckboard
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Office/Business
14
+ Classifier: Topic :: Internet :: WWW/HTTP
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: msal>=1.24
19
+ Requires-Dist: requests>=2.31
20
+ Dynamic: license-file
21
+
22
+ # graphkit
23
+
24
+ A small, dependency-light Python toolkit for authenticating to the
25
+ Microsoft Graph API using the **client credentials flow** (app-only
26
+ auth -- no user sign-in required), plus a thin HTTP client for making
27
+ calls against it.
28
+
29
+ This is useful for background scripts, scheduled jobs, or shared-mailbox
30
+ automations that need to act on Outlook mail, Planner tasks, SharePoint,
31
+ or any other Graph resource without a human logging in each time.
32
+
33
+ If this saves you time, consider [buying me a coffee](https://ko-fi.com/duckboard).
34
+
35
+ ## Features
36
+
37
+ - OAuth2 client credentials flow via [MSAL](https://github.com/AzureAD/microsoft-authentication-library-for-python)
38
+ - No secrets in code -- credentials are read from environment variables
39
+ - A minimal `GraphClient` for GET/POST/PATCH/DELETE against any Graph endpoint
40
+ - Two worked examples: drafting an email, creating a Planner task
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install -r requirements.txt
46
+ ```
47
+
48
+ or, to install as an editable package:
49
+
50
+ ```bash
51
+ pip install -e .
52
+ ```
53
+
54
+ ## Azure app registration
55
+
56
+ You need an Azure AD (Entra ID) app registration before this will work.
57
+ If you don't already have one:
58
+
59
+ 1. Go to [portal.azure.com](https://portal.azure.com) -> **Azure Active
60
+ Directory** (or **Entra ID**) -> **App registrations** -> **New
61
+ registration**.
62
+ 2. Give it a name, leave the default options, and click **Register**.
63
+ 3. Note down the **Application (client) ID** and **Directory (tenant)
64
+ ID** from the app's Overview page.
65
+ 4. Go to **Certificates & secrets** -> **New client secret**. Copy the
66
+ secret **value** immediately -- it's only shown once.
67
+ 5. Go to **API permissions** -> **Add a permission** -> **Microsoft
68
+ Graph** -> **Application permissions**, and add whichever
69
+ permissions your use case needs, for example:
70
+ - `Mail.ReadWrite` (draft/read/send mail as any mailbox)
71
+ - `Tasks.ReadWrite` (Planner tasks)
72
+ - `Group.Read.All` (look up Microsoft 365 Groups by name)
73
+ - `User.Read.All` (look up users by name/UPN)
74
+ 6. Click **Grant admin consent** for your organisation (this step
75
+ requires a Global Administrator or Application Administrator role).
76
+
77
+ Application permissions act on the whole tenant, scoped only by
78
+ whichever specific mailbox/site/group you address in your API calls --
79
+ there's no per-user consent screen for this flow. Only grant the
80
+ permissions you actually need.
81
+
82
+ ## Configuration
83
+
84
+ Copy `.env.example` to `.env` and fill in your values:
85
+
86
+ ```bash
87
+ cp .env.example .env
88
+ ```
89
+
90
+ ```
91
+ GRAPH_TENANT_ID=your-tenant-id
92
+ GRAPH_CLIENT_ID=your-client-id
93
+ GRAPH_CLIENT_SECRET=your-client-secret
94
+ ```
95
+
96
+ `.env` is already in `.gitignore` -- never commit real credentials.
97
+
98
+ To load `.env` automatically in your own scripts, install
99
+ `python-dotenv` and add this before importing graphkit:
100
+
101
+ ```python
102
+ from dotenv import load_dotenv
103
+ load_dotenv()
104
+ ```
105
+
106
+ Alternatively, just set the three variables directly in your shell or
107
+ process environment -- graphkit doesn't require python-dotenv, it only
108
+ reads `os.environ`.
109
+
110
+ ## Usage
111
+
112
+ ```python
113
+ from graphkit import get_token, GraphClient
114
+
115
+ token = get_token() # reads GRAPH_TENANT_ID / GRAPH_CLIENT_ID / GRAPH_CLIENT_SECRET
116
+ graph = GraphClient(token)
117
+
118
+ me = graph.get("/users/someone@yourdomain.com")
119
+ print(me["displayName"])
120
+ ```
121
+
122
+ `GraphClient` is intentionally minimal -- `.get()`, `.post()`,
123
+ `.patch()`, `.delete()` -- so it works with any Graph endpoint, not
124
+ just the ones this package happens to have examples for.
125
+
126
+ ### Examples
127
+
128
+ - `examples/send_email_draft.py` -- create a draft email in a mailbox's
129
+ Drafts folder
130
+ - `examples/create_planner_task.py` -- look up a Microsoft 365 Group,
131
+ Planner plan, and bucket by name, then create a task in it
132
+
133
+ Run either directly once you've set up your `.env`:
134
+
135
+ ```bash
136
+ python examples/send_email_draft.py
137
+ python examples/create_planner_task.py
138
+ ```
139
+
140
+ ## Testing
141
+
142
+ ```bash
143
+ pip install pytest
144
+ pytest
145
+ ```
146
+
147
+ Tests mock MSAL, so they run offline without real Azure credentials.
148
+
149
+ ## Why client credentials and not device code / interactive login?
150
+
151
+ Client credentials (app-only) auth is the right choice when a script
152
+ runs unattended -- no browser, no one available to click "allow" on a
153
+ sign-in prompt. If you need a script to act *as a specific signed-in
154
+ user* instead of a shared app identity, you'd want MSAL's device code
155
+ or interactive flows instead, which this package doesn't cover.
156
+
157
+ ## License
158
+
159
+ MIT -- see [LICENSE](LICENSE). Do whatever you like with this; a
160
+ credit or a [Ko-fi tip](https://ko-fi.com/duckboard) is
161
+ appreciated but never required.
@@ -0,0 +1,8 @@
1
+ graphkit/__init__.py,sha256=tQs-JF0ey5Hm5umro1EoDRLEE8xupr8l85xDK1zD-wE,489
2
+ graphkit/auth.py,sha256=SFEBs0mLMwLHvslzbREywntRWDMs-i0fGDdwAsyYcAU,2827
3
+ graphkit/client.py,sha256=B5Q7tcUlUxdROb96nGZk721LN2iDUut-3AY5kKaeVMg,3068
4
+ msgraphkit-0.1.0.dist-info/licenses/LICENSE,sha256=ChymOtwCDSjsL0VwJuqUn7dw8gqHr0Rt4jHcFsJa8Ec,1066
5
+ msgraphkit-0.1.0.dist-info/METADATA,sha256=OGDxzIqD_PSNxr_3vMi4m21Iqx99gUPgUow3ji2Ek_w,5475
6
+ msgraphkit-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
7
+ msgraphkit-0.1.0.dist-info/top_level.txt,sha256=xSiy0YBLQPHfTXAI0ihN8QO69oBv2zR-zqkQf9z2kvY,9
8
+ msgraphkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.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 Duckboard
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
+ graphkit