msgraphkit 0.1.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.
@@ -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,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,140 @@
1
+ # graphkit
2
+
3
+ A small, dependency-light Python toolkit for authenticating to the
4
+ Microsoft Graph API using the **client credentials flow** (app-only
5
+ auth -- no user sign-in required), plus a thin HTTP client for making
6
+ calls against it.
7
+
8
+ This is useful for background scripts, scheduled jobs, or shared-mailbox
9
+ automations that need to act on Outlook mail, Planner tasks, SharePoint,
10
+ or any other Graph resource without a human logging in each time.
11
+
12
+ If this saves you time, consider [buying me a coffee](https://ko-fi.com/duckboard).
13
+
14
+ ## Features
15
+
16
+ - OAuth2 client credentials flow via [MSAL](https://github.com/AzureAD/microsoft-authentication-library-for-python)
17
+ - No secrets in code -- credentials are read from environment variables
18
+ - A minimal `GraphClient` for GET/POST/PATCH/DELETE against any Graph endpoint
19
+ - Two worked examples: drafting an email, creating a Planner task
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install -r requirements.txt
25
+ ```
26
+
27
+ or, to install as an editable package:
28
+
29
+ ```bash
30
+ pip install -e .
31
+ ```
32
+
33
+ ## Azure app registration
34
+
35
+ You need an Azure AD (Entra ID) app registration before this will work.
36
+ If you don't already have one:
37
+
38
+ 1. Go to [portal.azure.com](https://portal.azure.com) -> **Azure Active
39
+ Directory** (or **Entra ID**) -> **App registrations** -> **New
40
+ registration**.
41
+ 2. Give it a name, leave the default options, and click **Register**.
42
+ 3. Note down the **Application (client) ID** and **Directory (tenant)
43
+ ID** from the app's Overview page.
44
+ 4. Go to **Certificates & secrets** -> **New client secret**. Copy the
45
+ secret **value** immediately -- it's only shown once.
46
+ 5. Go to **API permissions** -> **Add a permission** -> **Microsoft
47
+ Graph** -> **Application permissions**, and add whichever
48
+ permissions your use case needs, for example:
49
+ - `Mail.ReadWrite` (draft/read/send mail as any mailbox)
50
+ - `Tasks.ReadWrite` (Planner tasks)
51
+ - `Group.Read.All` (look up Microsoft 365 Groups by name)
52
+ - `User.Read.All` (look up users by name/UPN)
53
+ 6. Click **Grant admin consent** for your organisation (this step
54
+ requires a Global Administrator or Application Administrator role).
55
+
56
+ Application permissions act on the whole tenant, scoped only by
57
+ whichever specific mailbox/site/group you address in your API calls --
58
+ there's no per-user consent screen for this flow. Only grant the
59
+ permissions you actually need.
60
+
61
+ ## Configuration
62
+
63
+ Copy `.env.example` to `.env` and fill in your values:
64
+
65
+ ```bash
66
+ cp .env.example .env
67
+ ```
68
+
69
+ ```
70
+ GRAPH_TENANT_ID=your-tenant-id
71
+ GRAPH_CLIENT_ID=your-client-id
72
+ GRAPH_CLIENT_SECRET=your-client-secret
73
+ ```
74
+
75
+ `.env` is already in `.gitignore` -- never commit real credentials.
76
+
77
+ To load `.env` automatically in your own scripts, install
78
+ `python-dotenv` and add this before importing graphkit:
79
+
80
+ ```python
81
+ from dotenv import load_dotenv
82
+ load_dotenv()
83
+ ```
84
+
85
+ Alternatively, just set the three variables directly in your shell or
86
+ process environment -- graphkit doesn't require python-dotenv, it only
87
+ reads `os.environ`.
88
+
89
+ ## Usage
90
+
91
+ ```python
92
+ from graphkit import get_token, GraphClient
93
+
94
+ token = get_token() # reads GRAPH_TENANT_ID / GRAPH_CLIENT_ID / GRAPH_CLIENT_SECRET
95
+ graph = GraphClient(token)
96
+
97
+ me = graph.get("/users/someone@yourdomain.com")
98
+ print(me["displayName"])
99
+ ```
100
+
101
+ `GraphClient` is intentionally minimal -- `.get()`, `.post()`,
102
+ `.patch()`, `.delete()` -- so it works with any Graph endpoint, not
103
+ just the ones this package happens to have examples for.
104
+
105
+ ### Examples
106
+
107
+ - `examples/send_email_draft.py` -- create a draft email in a mailbox's
108
+ Drafts folder
109
+ - `examples/create_planner_task.py` -- look up a Microsoft 365 Group,
110
+ Planner plan, and bucket by name, then create a task in it
111
+
112
+ Run either directly once you've set up your `.env`:
113
+
114
+ ```bash
115
+ python examples/send_email_draft.py
116
+ python examples/create_planner_task.py
117
+ ```
118
+
119
+ ## Testing
120
+
121
+ ```bash
122
+ pip install pytest
123
+ pytest
124
+ ```
125
+
126
+ Tests mock MSAL, so they run offline without real Azure credentials.
127
+
128
+ ## Why client credentials and not device code / interactive login?
129
+
130
+ Client credentials (app-only) auth is the right choice when a script
131
+ runs unattended -- no browser, no one available to click "allow" on a
132
+ sign-in prompt. If you need a script to act *as a specific signed-in
133
+ user* instead of a shared app identity, you'd want MSAL's device code
134
+ or interactive flows instead, which this package doesn't cover.
135
+
136
+ ## License
137
+
138
+ MIT -- see [LICENSE](LICENSE). Do whatever you like with this; a
139
+ credit or a [Ko-fi tip](https://ko-fi.com/duckboard) is
140
+ appreciated but never required.
@@ -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"
@@ -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"]
@@ -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,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ graphkit/__init__.py
5
+ graphkit/auth.py
6
+ graphkit/client.py
7
+ msgraphkit.egg-info/PKG-INFO
8
+ msgraphkit.egg-info/SOURCES.txt
9
+ msgraphkit.egg-info/dependency_links.txt
10
+ msgraphkit.egg-info/requires.txt
11
+ msgraphkit.egg-info/top_level.txt
12
+ tests/test_auth.py
@@ -0,0 +1,2 @@
1
+ msal>=1.24
2
+ requests>=2.31
@@ -0,0 +1 @@
1
+ graphkit
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "msgraphkit"
7
+ version = "0.1.0"
8
+ description = "A small, dependency-light toolkit for authenticating to the Microsoft Graph API (app-only / client credentials flow) and making simple REST calls against it."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Duckboard" }
14
+ ]
15
+ dependencies = [
16
+ "msal>=1.24",
17
+ "requests>=2.31",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Topic :: Office/Business",
25
+ "Topic :: Internet :: WWW/HTTP",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/Duckboard/graphkit"
30
+ "Buy me a coffee" = "https://ko-fi.com/duckboard"
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["graphkit*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,77 @@
1
+ """
2
+ Tests for graphkit.auth.
3
+
4
+ Run with: pytest
5
+
6
+ These tests don't call the real Azure endpoint -- they mock msal so
7
+ they run offline and don't need real credentials.
8
+ """
9
+
10
+ import os
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ import pytest
14
+
15
+ from graphkit.auth import get_token, GraphAuthError
16
+
17
+
18
+ def test_get_token_raises_without_env_vars(monkeypatch):
19
+ monkeypatch.delenv("GRAPH_TENANT_ID", raising=False)
20
+ monkeypatch.delenv("GRAPH_CLIENT_ID", raising=False)
21
+ monkeypatch.delenv("GRAPH_CLIENT_SECRET", raising=False)
22
+
23
+ with pytest.raises(GraphAuthError, match="GRAPH_TENANT_ID"):
24
+ get_token()
25
+
26
+
27
+ @patch("graphkit.auth.msal.ConfidentialClientApplication")
28
+ def test_get_token_returns_access_token(mock_app_cls, monkeypatch):
29
+ monkeypatch.setenv("GRAPH_TENANT_ID", "fake-tenant")
30
+ monkeypatch.setenv("GRAPH_CLIENT_ID", "fake-client")
31
+ monkeypatch.setenv("GRAPH_CLIENT_SECRET", "fake-secret")
32
+
33
+ mock_app = MagicMock()
34
+ mock_app.acquire_token_for_client.return_value = {"access_token": "fake-token-value"}
35
+ mock_app_cls.return_value = mock_app
36
+
37
+ token = get_token()
38
+
39
+ assert token == "fake-token-value"
40
+ mock_app_cls.assert_called_once_with(
41
+ "fake-client",
42
+ authority="https://login.microsoftonline.com/fake-tenant",
43
+ client_credential="fake-secret",
44
+ )
45
+
46
+
47
+ @patch("graphkit.auth.msal.ConfidentialClientApplication")
48
+ def test_get_token_raises_on_auth_failure(mock_app_cls, monkeypatch):
49
+ monkeypatch.setenv("GRAPH_TENANT_ID", "fake-tenant")
50
+ monkeypatch.setenv("GRAPH_CLIENT_ID", "fake-client")
51
+ monkeypatch.setenv("GRAPH_CLIENT_SECRET", "wrong-secret")
52
+
53
+ mock_app = MagicMock()
54
+ mock_app.acquire_token_for_client.return_value = {
55
+ "error": "invalid_client",
56
+ "error_description": "AADSTS7000215: Invalid client secret provided.",
57
+ }
58
+ mock_app_cls.return_value = mock_app
59
+
60
+ with pytest.raises(GraphAuthError, match="Invalid client secret"):
61
+ get_token()
62
+
63
+
64
+ def test_get_token_accepts_explicit_args(monkeypatch):
65
+ # Explicit args should be used even if env vars are unset.
66
+ monkeypatch.delenv("GRAPH_TENANT_ID", raising=False)
67
+ monkeypatch.delenv("GRAPH_CLIENT_ID", raising=False)
68
+ monkeypatch.delenv("GRAPH_CLIENT_SECRET", raising=False)
69
+
70
+ with patch("graphkit.auth.msal.ConfidentialClientApplication") as mock_app_cls:
71
+ mock_app = MagicMock()
72
+ mock_app.acquire_token_for_client.return_value = {"access_token": "explicit-token"}
73
+ mock_app_cls.return_value = mock_app
74
+
75
+ token = get_token(tenant_id="t", client_id="c", client_secret="s")
76
+
77
+ assert token == "explicit-token"