ms-graph-toolbox 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.
File without changes
@@ -0,0 +1,173 @@
1
+ import requests
2
+ import base64
3
+ import os
4
+ import mimetypes
5
+
6
+
7
+ def send_email(gph_object,
8
+ subject,
9
+ content_type,
10
+ body,
11
+ sender,
12
+ to_field,
13
+ cc_field=None,
14
+ bcc_field=None,
15
+ priority="Normal",
16
+ attachments=None):
17
+ """
18
+ Send an email using Microsoft Graph on behalf of `sender`.
19
+
20
+ Parameters:
21
+ logger: logger instance to record actions (debug/error).
22
+ subject: email subject string.
23
+ content_type: one of 'text', 'html', 'text/plain', 'text/html', etc.
24
+ body: email body string.
25
+ to_field, cc_field, bcc_field: comma-separated recipient strings (e.g. "a@x.com, b@y.com").
26
+ priority: message importance, e.g. "Low", "Normal", "High".
27
+ attachments: optional list of attachment descriptors. Each descriptor may be:
28
+ - {'path': 'C:\\full\\path\\file.pdf', 'name': 'file.pdf', 'content_type': 'application/pdf', 'inline': False}
29
+ - {'content_bytes': b'...', 'name': 'image.png', 'content_type': 'image/png', 'inline': True, 'content_id': 'img1'}
30
+ For inline images set 'inline': True and reference them in an HTML body as <img src="cid:content_id">.
31
+ If content_type not provided it will be guessed from the filename.
32
+ The code will base64-encode file contents as required by Graph.
33
+
34
+ Returns:
35
+ 0 on success (202 response), 1 on exception, 2 if no access token, 3 on non-202 HTTP response.
36
+ """
37
+ try:
38
+ # Ensure we have an access token before attempting to send
39
+ if not gph_object.access_token:
40
+ gph_object.logger.error("Invalid Access Token, email cannot be sent!")
41
+ return 2
42
+
43
+ # Parse recipient fields into Graph-friendly lists
44
+ to_recipients = parse_recipients(to_field)
45
+ cc_recipients = parse_recipients(cc_field)
46
+ bcc_recipients = parse_recipients(bcc_field)
47
+
48
+ # Normalize content type to one of Graph's expected values ("Text" or "HTML"), use "Text" as default
49
+ valid_types = {"text": "Text", "plain": "Text", "html": "HTML", "text/plain": "Text", "text/html": "HTML"}
50
+ content_type_normalized = valid_types.get(content_type.strip().lower(), "Text")
51
+
52
+ # Process attachments and if any contain inline items but the body is not HTML, log a warning
53
+ inline_found = False
54
+ attachments_payload = []
55
+ if attachments:
56
+ # Process all attachments in one pass
57
+ attachments_payload = [
58
+ att for desc in attachments
59
+ if (att := build_attachment(descriptor=desc, logger=gph_object.logger)) is not None
60
+ ]
61
+
62
+ # Check for inline attachments
63
+ inline_found = any(att.get("isInline") for att in attachments_payload)
64
+
65
+ # Log warning if inline attachments found with non-HTML content
66
+ if inline_found and content_type_normalized != "HTML":
67
+ gph_object.logger.warning("Inline attachments present but body is not HTML. Inline images require HTML body and <img src=\"cid:contentId\"> references.")
68
+
69
+ # Build sendMail endpoint for the configured sender user
70
+ endpoint = f"https://graph.microsoft.com/v1.0/users/{sender}/sendMail"
71
+
72
+ # Compose message payload according to Graph sendMail schema
73
+ email_msg = {
74
+ "message": {
75
+ "subject": subject,
76
+ "body": {
77
+ "contentType": content_type_normalized,
78
+ "content": body
79
+ },
80
+ "toRecipients": to_recipients,
81
+ "importance": priority
82
+ }
83
+ }
84
+
85
+ # Attach cc/bcc only if provided
86
+ if cc_recipients:
87
+ email_msg["message"]["ccRecipients"] = cc_recipients
88
+ if bcc_recipients:
89
+ email_msg["message"]["bccRecipients"] = bcc_recipients
90
+
91
+ # Attach attachments if any
92
+ if attachments_payload:
93
+ email_msg["message"]["attachments"] = attachments_payload
94
+
95
+ # Set authorization header with Bearer token
96
+ headers = {"Authorization": f"Bearer {gph_object.access_token}", "Content-Type": "application/json"}
97
+ gph_object.logger.debug(f"Sending email using MS Graph from {sender}")
98
+ response = requests.post(endpoint, headers=headers, json=email_msg)
99
+
100
+ # 202 Accepted indicates Graph accepted the send request
101
+ if response.status_code == 202:
102
+ gph_object.logger.debug("Sending successful!")
103
+ return 0
104
+ else:
105
+ # Log status and response body for debugging failures
106
+ gph_object.logger.error(f"Sending Failed: {response.status_code}, {response.text}")
107
+ return 3
108
+ except Exception as e:
109
+ # Any unexpected exception during send is logged
110
+ gph_object.logger.error(f"Sending Failed: {e}")
111
+ return 1
112
+
113
+
114
+ # Convert a comma-separated string of addresses into the Graph recipient JSON format.
115
+ def parse_recipients(field):
116
+ if not field:
117
+ return []
118
+ return [{"emailAddress": {"address": addr.strip()}} for addr in field.split(",") if addr.strip()]
119
+
120
+
121
+ # Build Graph attachment payloads from descriptors
122
+ def build_attachment(descriptor, logger):
123
+ """
124
+ Accepts either:
125
+ - {'path': 'C:\\file', ...}
126
+ - {'content_bytes': b'...', ...}
127
+ Returns a dict suitable for Graph message attachments.
128
+ """
129
+ try:
130
+ # Basic check
131
+ if "path" not in descriptor and "content_bytes" not in descriptor:
132
+ logger.error(f"build_attachment failed: Descriptor missing 'path' or 'content_bytes': {descriptor}")
133
+ return None
134
+ # Determine name
135
+ name = descriptor.get("name")
136
+ content_type_guess = None
137
+
138
+ # Load bytes from file path if provided
139
+ if "path" in descriptor and descriptor["path"]:
140
+ path = descriptor["path"]
141
+ with open(path, "rb") as f:
142
+ data = f.read()
143
+ if not name:
144
+ name = os.path.basename(path)
145
+ content_type_guess = mimetypes.guess_type(path)[0]
146
+ elif "content_bytes" in descriptor and descriptor["content_bytes"] is not None:
147
+ data = descriptor["content_bytes"]
148
+ if isinstance(data, str):
149
+ data = data.encode("utf-8")
150
+
151
+ # Determine contentType
152
+ content_type = descriptor.get("content_type") or content_type_guess or "application/octet-stream"
153
+
154
+ # Base64 encode content
155
+ content_bytes_b64 = base64.b64encode(data).decode("utf-8")
156
+
157
+ # Build attachment object (fileAttachment)
158
+ attachment = {
159
+ "@odata.type": "#microsoft.graph.fileAttachment",
160
+ "name": name or "attachment",
161
+ "contentType": content_type,
162
+ "contentBytes": content_bytes_b64
163
+ }
164
+
165
+ # Inline settings for images
166
+ if descriptor.get("inline"):
167
+ attachment["isInline"] = True
168
+ # contentId is used as cid reference in HTML body: <img src="cid:contentId">
169
+ attachment["contentId"] = descriptor.get("content_id") or (name or "inline")
170
+ return attachment
171
+ except Exception as e:
172
+ logger.error(f"build_attachment: failed for descriptor {descriptor}: {e}")
173
+ return None
@@ -0,0 +1,109 @@
1
+ from urllib.parse import quote
2
+ import pathlib as pl
3
+ import requests
4
+
5
+
6
+ class graph_sharepoint:
7
+ def __init__(self, access_token:str, logger):
8
+ self.access_token = access_token
9
+ self.logger = logger
10
+
11
+
12
+ def get_site_id(self, site_url:str):
13
+ # Request site ID
14
+ try:
15
+ full_url = f'https://graph.microsoft.com/v1.0/sites/{site_url}'
16
+ response = requests.get(full_url,
17
+ headers={'Authorization': f'Bearer {self.access_token}'})
18
+ self.logger.debug(f"get_site_id response: {response.status_code} - {response.text}")
19
+ return response.json().get('id') # Return the site ID
20
+ except Exception as e:
21
+ self.logger.error(f"get_site_id failed: {e}")
22
+ return None
23
+
24
+
25
+ def get_document_libraries(self, site_id:str):
26
+ # Retrieve drive IDs and names associated with a site
27
+ try:
28
+ drives_url = f'https://graph.microsoft.com/v1.0/sites/{site_id}/drives'
29
+ response = requests.get(drives_url, headers={'Authorization': f'Bearer {self.access_token}'})
30
+ drives = response.json().get('value', [])
31
+ return [(drive['id'], drive['name']) for drive in drives]
32
+ except Exception as e:
33
+ self.logger.error(f"get_document_libraries failed: {e}")
34
+ return None
35
+
36
+
37
+ def get_folder_content(self, site_id:str, drive_id:str):
38
+ # Get the contents of a folder
39
+ try:
40
+ folder_url = f'https://graph.microsoft.com/v1.0/sites/{site_id}/drives/{drive_id}/root/children'
41
+ response = requests.get(folder_url, headers={'Authorization': f'Bearer {self.access_token}'})
42
+ return response.json().get('value', [])
43
+ except Exception as e:
44
+ self.logger.error(f"get_folder_content failed: {e}")
45
+ return None
46
+
47
+ def print_folder_content(self, folder_content):
48
+ # Display the contents of a SharePoint folder
49
+ folders = []
50
+ files = []
51
+
52
+ for item in folder_content:
53
+ if "folder" in item:
54
+ folders.append(item["name"])
55
+ elif "file" in item:
56
+ files.append(item["name"])
57
+
58
+ print(f"Folders: {len(folders)}")
59
+ for f in sorted(folders):
60
+ print(f)
61
+
62
+ print(f"Files: {len(files)}")
63
+ for f in sorted(files):
64
+ print(f)
65
+
66
+
67
+ def upload_file_graph(self, site_id, drive_id:str, folder_path:str, local_file_path:str):
68
+ """
69
+ Uploads a file to a SharePoint folder using Microsoft Graph API.
70
+
71
+ :param site_id: The SharePoint site ID
72
+ :param drive_id: The drive ID for the desired folder
73
+ :param folder_path: Path inside taht folder
74
+ :param local_file_path: Local path of the file to be uploaded
75
+ :return: (file_url, success_file_count)
76
+ """
77
+ try:
78
+ file_path = pl.Path(local_file_path)
79
+ if not file_path.is_file():
80
+ raise FileNotFoundError(f"File not found: {file_path}")
81
+
82
+ with open(file_path, "rb") as file:
83
+ file_content = file.read()
84
+
85
+ # Build the upload URL
86
+ folder_path_encoded = quote(folder_path)
87
+ upload_url = (
88
+ f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives/{drive_id}/root:/{folder_path_encoded}/{file_path.name}:/content"
89
+ )
90
+
91
+ headers = {
92
+ "Authorization": f"Bearer {self.access_token}",
93
+ "Content-Type": "application/octet-stream"
94
+ }
95
+
96
+ response = requests.put(upload_url, headers=headers, data=file_content)
97
+ if response.status_code in [200, 201]:
98
+ file_info = response.json()
99
+ file_url = file_info.get("webUrl", "")
100
+ self.logger.debug(f"File uploaded to: {file_url}")
101
+ return file_url, 1
102
+ else:
103
+ self.logger.error(f"Upload to {upload_url} failed: {response.text}")
104
+ return response.text, 0
105
+ except FileNotFoundError as e:
106
+ return str(e), 0
107
+ except Exception as e:
108
+ self.logger.error(f"upload_file_graph failed: {e}")
109
+ return str(e), 0
@@ -0,0 +1,109 @@
1
+ import requests
2
+
3
+
4
+ def get_users(gph_object,
5
+ select_data: str | None = None,
6
+ search_name: str | None = None,
7
+ search_title: str | None = None,
8
+ search_email: str | None = None,
9
+ search_alias: str | None = None,
10
+ search_company: str | None = None
11
+ ) -> list[dict] | None:
12
+ """
13
+ Search for users in Microsoft Graph based on provided criteria.
14
+
15
+ Notes:
16
+ - companyName is not reliably supported in server-side $filter for all tenants/APIs,
17
+ so company filtering is applied client-side to avoid "unsupported filter" errors.
18
+ - This function uses `startswith(...)` for server-side partial matching where possible
19
+ and escapes single quotes in filter values.
20
+
21
+ Args:
22
+ gph_object: An initialized ms_graph_toolbox object with valid access_token and logger.
23
+ select_data: Comma-separated string or a list of properties to return (e.g. "displayName,mail,jobTitle").
24
+ search_name: Filter users by displayName (partial, startswith).
25
+ search_title: Filter users by jobTitle (partial, startswith).
26
+ search_email: Filter users by mail (partial, startswith).
27
+ search_alias: Filter users by alias (mailNickname / userPrincipalName / proxyAddresses) (exact or startswith).
28
+ search_company: Filter users by companyName (partial, case-insensitive). Applied client-side.
29
+
30
+ Returns:
31
+ List of user dicts matching criteria, or None on error.
32
+ """
33
+ try:
34
+ def esc(val: str) -> str:
35
+ return val.replace("'", "''")
36
+
37
+ if not any([search_name, search_title, search_email, search_alias, search_company]):
38
+ gph_object.logger.warning("No filters provided; retrieving all users may be slow in large organizations.")
39
+
40
+ # Build server-side filters (exclude companyName to avoid unsupported-filter errors)
41
+ server_filters = []
42
+ if search_name:
43
+ server_filters.append(f"startswith(displayName,'{esc(search_name)}')")
44
+ if search_title:
45
+ server_filters.append(f"startswith(jobTitle,'{esc(search_title)}')")
46
+ if search_email:
47
+ server_filters.append(f"startswith(mail,'{esc(search_email)}')")
48
+ if search_alias:
49
+ # Try matching common alias forms
50
+ alias_escaped = esc(search_alias)
51
+ server_filters.append(
52
+ f"(startswith(userPrincipalName,'{alias_escaped}') or startswith(mailNickname,'{alias_escaped}') "
53
+ f"or proxyAddresses/any(x:x eq 'smtp:{alias_escaped}') or proxyAddresses/any(x:x eq 'SMTP:{alias_escaped}'))"
54
+ )
55
+
56
+ endpoint = "https://graph.microsoft.com/v1.0/users"
57
+ params = {}
58
+ if server_filters:
59
+ params["$filter"] = " and ".join(server_filters)
60
+ if select_data:
61
+ if isinstance(select_data, list):
62
+ params["$select"] = ",".join(select_data)
63
+ else:
64
+ params["$select"] = select_data
65
+ # Include count if desired (note: some endpoints require ConsistencyLevel header for $count)
66
+ params["$count"] = "true"
67
+
68
+ headers = {"Authorization": f"Bearer {gph_object.access_token}",
69
+ "ConsistencyLevel": "eventual"}
70
+ users_list = []
71
+
72
+ # Use params on first request; if @odata.nextLink is returned, follow it directly (it already contains params)
73
+ url = endpoint
74
+ first = True
75
+ while True:
76
+ if first:
77
+ resp = requests.get(url, headers=headers, params=params)
78
+ first = False
79
+ else:
80
+ resp = requests.get(url, headers=headers) # nextLink already has query
81
+ if resp.status_code != 200:
82
+ gph_object.logger.error(f"Failed to retrieve users: {resp.status_code} - {resp.text}")
83
+ return None
84
+
85
+ data = resp.json()
86
+ users = data.get("value", [])
87
+ users_list.extend(users)
88
+
89
+ next_link = data.get("@odata.nextLink")
90
+ if not next_link:
91
+ break
92
+ url = next_link
93
+
94
+ # Apply company filter client-side (case-insensitive, partial match) if requested
95
+ if search_company:
96
+ sc = search_company.lower()
97
+ users_list = [u for u in users_list if sc in (u.get("companyName") or "").lower()]
98
+
99
+ gph_object.logger.debug(f"Retrieved {len(users_list)} users")
100
+ return users_list
101
+
102
+ except Exception as e:
103
+ # Log exception details
104
+ try:
105
+ gph_object.logger.error(f"Exception occurred while searching for users: {e}")
106
+ except Exception:
107
+ # Fallback if logger not available
108
+ pass
109
+ return None
@@ -0,0 +1,57 @@
1
+ """
2
+ Basic code to obtain an application token via MSAL to be used with Microsoft Graph.
3
+
4
+ """
5
+ import msal
6
+
7
+
8
+ class ms_graph_toolbox:
9
+ """
10
+ Wrapper class to handle app-only authentication and sending email via Microsoft Graph.
11
+
12
+ Attributes:
13
+ logger: Logger with .debug/.info/.warning/.error methods for logging.
14
+ sender: The user (email) that will be used as the "From" / mailbox to send from.
15
+ client_id: Azure AD Application (client) ID used for the OAuth2 client credentials flow.
16
+ client_secret: Confidential value (application secret) for the Azure AD app used to authenticate the app.
17
+ tenant_id: Azure AD tenant identifier (GUID) or tenant domain used to build the authority URL
18
+ """
19
+
20
+ def __init__(self,
21
+ client_id,
22
+ client_secret,
23
+ tenant_id,
24
+ logger):
25
+
26
+ # Set logger and access token placeholder
27
+ self.logger = logger
28
+ self.access_token = None
29
+
30
+ try:
31
+ # Build authority URL for tenant
32
+ authority = f"https://login.microsoftonline.com/{tenant_id}"
33
+ # Use .default scope for client credentials to get app-level permissions
34
+ scopes = ["https://graph.microsoft.com/.default"]
35
+
36
+ # Create MSAL confidential client app using client credentials
37
+ app = msal.ConfidentialClientApplication(
38
+ client_id,
39
+ authority=authority,
40
+ client_credential=client_secret
41
+ )
42
+
43
+ # Acquire token for client (app-only)
44
+ result = app.acquire_token_for_client(scopes)
45
+
46
+ # If token obtained, store it. Otherwise log the error.
47
+ if "access_token" in result:
48
+ self.access_token = result["access_token"]
49
+ logger.debug("Successfully obtained Graph API token.")
50
+ else:
51
+ # error_description may contain helpful details about why token request failed
52
+ error_msg = result.get("error_description", str(result))
53
+ logger.error(f"Failed to get token: {error_msg}")
54
+
55
+ except Exception as e:
56
+ # Catch-all to ensure initialization failure is logged
57
+ logger.error(f"graph_emailer initialization failed: {e}")
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: ms-graph-toolbox
3
+ Version: 0.1.0
4
+ Summary: A lightweight Python wrapper around Microsoft Graph for email, users, and SharePoint operations.
5
+ Author: runway28R
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/runway28R/ms-graph-toolbox
8
+ Project-URL: Source, https://github.com/runway28R/ms-graph-toolbox
9
+ Project-URL: Issues, https://github.com/runway28R/ms-graph-toolbox/issues
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: requests>=2.31.0
14
+ Requires-Dist: msal>=1.27.0
15
+ Dynamic: license-file
16
+
17
+ # Microsoft Graph Toolbox
18
+
19
+ A lightweight Python wrapper around Microsoft Graph for sending email, retrieving users, and working with SharePoint files.
20
+
21
+ ## Features
22
+
23
+ - Authenticate with Microsoft Graph using Microsoft Entra ID application credentials
24
+ - Send email messages
25
+ - Retrieve users from Microsoft Graph
26
+ - List SharePoint sites and document libraries
27
+ - Browse folders and upload files to SharePoint
28
+
29
+ ## Requirements
30
+
31
+ - Python 3.12 or newer
32
+ - A Microsoft Entra ID application registration
33
+ - Microsoft Graph API permissions
34
+ - A Microsoft 365 account with access to the required resources
35
+
36
+ ## Installation
37
+
38
+ Install the package from PyPI:
39
+
40
+ ```bash
41
+ pip install ms-graph-toolbox
42
+ ```
43
+
44
+ ## Authentication
45
+
46
+ The package uses application credentials:
47
+
48
+ - Client ID
49
+ - Client secret
50
+ - Tenant ID
51
+
52
+ Make sure the application has the required Microsoft Graph permissions and that administrator consent has been granted where necessary.
53
+
54
+ ## Basic Usage
55
+
56
+ ```python
57
+ import logging
58
+ import os
59
+
60
+ from ms_graph_toolbox.ms_graph_toolbox import ms_graph_toolbox
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ graph = ms_graph_toolbox(
65
+ client_id=os.environ["MS_GRAPH_CLIENT_ID"],
66
+ client_secret=os.environ["MS_GRAPH_CLIENT_SECRET"],
67
+ tenant_id=os.environ["MS_GRAPH_TENANT_ID"],
68
+ logger=logger,
69
+ )
70
+ ```
71
+
72
+ The object automatically obtains an access token that is used by the other package functions.
73
+
74
+ ## Sending Email
75
+
76
+ ```python
77
+ from ms_graph_toolbox.graph_email import send_email
78
+
79
+ send_email(
80
+ gph_object=graph,
81
+ subject="Test message",
82
+ content_type="Text",
83
+ body="This message was sent using Microsoft Graph.",
84
+ sender="sender@example.com",
85
+ to_field="recipient@example.com",
86
+ )
87
+ ```
88
+
89
+ Additional options include carbon-copy recipients, blind-carbon-copy recipients, message priority, and file attachments.
90
+
91
+ Multiple recipients can be provided as a comma-separated string:
92
+
93
+ ```python
94
+ send_email(
95
+ gph_object=graph,
96
+ subject="Team update",
97
+ content_type="Text",
98
+ body="This message was sent to multiple recipients.",
99
+ sender="sender@example.com",
100
+ to_field="first@example.com, second@example.com",
101
+ )
102
+ ```
103
+
104
+ ## Retrieving Users
105
+
106
+ ```python
107
+ from ms_graph_toolbox.graph_users import get_users
108
+
109
+ users = get_users(graph)
110
+
111
+ for user in users:
112
+ print(user)
113
+ ```
114
+
115
+ You can also filter the results by name, title, email address, alias, company, or selected fields:
116
+
117
+ ```python
118
+ users = get_users(
119
+ graph,
120
+ select_data=["displayName", "mail", "jobTitle"],
121
+ search_name="Alex",
122
+ )
123
+ ```
124
+
125
+ ## SharePoint Operations
126
+
127
+ SharePoint functionality is available through the `graph_sharepoint` class:
128
+
129
+ ```python
130
+ from ms_graph_toolbox.graph_sharepoint import graph_sharepoint
131
+
132
+ sharepoint = graph_sharepoint(
133
+ access_token=graph.access_token,
134
+ logger=logger,
135
+ )
136
+ ```
137
+
138
+ The SharePoint tools support:
139
+
140
+ - Finding a SharePoint site
141
+ - Listing document libraries
142
+ - Browsing folder contents
143
+ - Uploading files
144
+
145
+ See the [examples](https://github.com/runway28R/ms-graph-toolbox/tree/main/examples) directory for complete SharePoint usage examples.
146
+
147
+ ## Examples
148
+
149
+ The repository contains complete examples for:
150
+
151
+ - Sending email
152
+ - Retrieving users
153
+ - Uploading files to SharePoint
154
+
155
+ The examples are available in the [examples](https://github.com/runway28R/ms-graph-toolbox/tree/main/examples) directory.
156
+
157
+ ## Configuration
158
+
159
+ For local development, set these environment variables before running the examples.
160
+
161
+ ### PowerShell
162
+
163
+ ```powershell
164
+ $env:MS_GRAPH_CLIENT_ID = "your-client-id"
165
+ $env:MS_GRAPH_CLIENT_SECRET = "your-client-secret"
166
+ $env:MS_GRAPH_TENANT_ID = "your-tenant-id"
167
+ ```
168
+
169
+ ### Bash
170
+
171
+ ```bash
172
+ export MS_GRAPH_CLIENT_ID="your-client-id"
173
+ export MS_GRAPH_CLIENT_SECRET="your-client-secret"
174
+ export MS_GRAPH_TENANT_ID="your-tenant-id"
175
+ ```
176
+
177
+ Do not commit client secrets or other credentials to the repository.
178
+
179
+ ## License
180
+
181
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,10 @@
1
+ ms_graph_toolbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ms_graph_toolbox/graph_email.py,sha256=SBCKsGf_rzqYgD9H5_N7B3sCrhGei6liO626S3H4k-0,7521
3
+ ms_graph_toolbox/graph_sharepoint.py,sha256=j4XKcvFEb3ithrQMv4KF1Tu03R6HXx7NUq3ImhyCJnM,4345
4
+ ms_graph_toolbox/graph_users.py,sha256=tuwLaBXqgsm0vCZDS6ByK_G7izCjWumvuhQtaFVZHpw,4861
5
+ ms_graph_toolbox/ms_graph_toolbox.py,sha256=5y9Ft1JLCntcPcATCfEf7NDzMoQaRaBYAlUBstNhpmU,2361
6
+ ms_graph_toolbox-0.1.0.dist-info/licenses/LICENSE,sha256=D-rqtNjpHqJK4pXKCW9x_aHi0JsjhpbpmzL4uSpgEsg,1087
7
+ ms_graph_toolbox-0.1.0.dist-info/METADATA,sha256=u3mRjhEdOdEri6sUYufLBHAma1vOHjdpl4yFREIPfaU,4660
8
+ ms_graph_toolbox-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ ms_graph_toolbox-0.1.0.dist-info/top_level.txt,sha256=DEG-OQ73sCMidsbyVye0y05oZVXeFOUVWhkzmlrhZAw,17
10
+ ms_graph_toolbox-0.1.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) 2025 runway28R
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
+ ms_graph_toolbox