mgraphctl 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.

Potentially problematic release.


This version of mgraphctl might be problematic. Click here for more details.

Files changed (57) hide show
  1. mgraphctl/__init__.py +3 -0
  2. mgraphctl/__main__.py +3 -0
  3. mgraphctl/auth.py +395 -0
  4. mgraphctl/cli.py +259 -0
  5. mgraphctl/commands/__init__.py +71 -0
  6. mgraphctl/commands/api.py +164 -0
  7. mgraphctl/commands/calendar.py +491 -0
  8. mgraphctl/commands/chats.py +329 -0
  9. mgraphctl/commands/config_cmd.py +140 -0
  10. mgraphctl/commands/groups.py +65 -0
  11. mgraphctl/commands/mail.py +705 -0
  12. mgraphctl/commands/mailbox.py +176 -0
  13. mgraphctl/commands/meetings.py +294 -0
  14. mgraphctl/commands/onedrive.py +351 -0
  15. mgraphctl/commands/onenote.py +196 -0
  16. mgraphctl/commands/org.py +102 -0
  17. mgraphctl/commands/people.py +201 -0
  18. mgraphctl/commands/planner.py +268 -0
  19. mgraphctl/commands/presence.py +102 -0
  20. mgraphctl/commands/search.py +75 -0
  21. mgraphctl/commands/sharepoint.py +304 -0
  22. mgraphctl/commands/teams.py +235 -0
  23. mgraphctl/commands/todo.py +253 -0
  24. mgraphctl/commands/top.py +284 -0
  25. mgraphctl/config.py +385 -0
  26. mgraphctl/errors.py +276 -0
  27. mgraphctl/fixtures.py +149 -0
  28. mgraphctl/graph/__init__.py +1 -0
  29. mgraphctl/graph/calendar.py +313 -0
  30. mgraphctl/graph/chats.py +447 -0
  31. mgraphctl/graph/files.py +269 -0
  32. mgraphctl/graph/groups.py +55 -0
  33. mgraphctl/graph/mail.py +663 -0
  34. mgraphctl/graph/mailbox.py +89 -0
  35. mgraphctl/graph/meetings.py +269 -0
  36. mgraphctl/graph/onedrive.py +35 -0
  37. mgraphctl/graph/onenote.py +138 -0
  38. mgraphctl/graph/org.py +86 -0
  39. mgraphctl/graph/people.py +83 -0
  40. mgraphctl/graph/planner.py +415 -0
  41. mgraphctl/graph/presence.py +89 -0
  42. mgraphctl/graph/search.py +178 -0
  43. mgraphctl/graph/sharepoint.py +294 -0
  44. mgraphctl/graph/teams.py +240 -0
  45. mgraphctl/graph/todo.py +199 -0
  46. mgraphctl/graph/users.py +85 -0
  47. mgraphctl/html.py +127 -0
  48. mgraphctl/http.py +843 -0
  49. mgraphctl/odata.py +72 -0
  50. mgraphctl/render.py +627 -0
  51. mgraphctl/resolve.py +111 -0
  52. mgraphctl/token_store.py +196 -0
  53. mgraphctl-0.1.0.dist-info/METADATA +226 -0
  54. mgraphctl-0.1.0.dist-info/RECORD +57 -0
  55. mgraphctl-0.1.0.dist-info/WHEEL +4 -0
  56. mgraphctl-0.1.0.dist-info/entry_points.txt +3 -0
  57. mgraphctl-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,269 @@
1
+ """Shared drive-item operations for OneDrive and SharePoint (spec §8.11, §8.12).
2
+
3
+ Pure: client + params in, Graph dicts / PageResult / Plan out. `base` is one of `/me/drive`,
4
+ `/drives/{id}` or `/sites/{id}/drive`, already encoded by the caller.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from math import ceil
11
+ from mimetypes import guess_type
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from mgraphctl import config, odata, resolve
16
+ from mgraphctl.errors import UsageError
17
+ from mgraphctl.http import (
18
+ JSON_HEADERS,
19
+ DownloadResult,
20
+ GraphClient,
21
+ PageResult,
22
+ Plan,
23
+ PlannedRequest,
24
+ )
25
+ from mgraphctl.render import Column, fmt_dt, fmt_size, to_iso_offset
26
+
27
+ ITEM_SELECT = "id,name,size,lastModifiedDateTime,file,folder,webUrl,parentReference"
28
+ PAGE_LS, CAP_LS = 200, 1000
29
+ CONFLICT_BEHAVIORS = frozenset({"rename", "replace", "fail"})
30
+
31
+
32
+ def item_ref(base: str, ref: str) -> str:
33
+ """`{base}/items/{id}` for an id (or `id:`-forced value), else `{base}/root:/{path}`."""
34
+ value, forced = resolve.split_id_prefix(ref)
35
+ if resolve.looks_like_id(ref, "drive_item"):
36
+ return f"{base}/items" + odata.p(value)
37
+ return f"{base}/root:/{odata.drive_path(value.lstrip('/'))}"
38
+
39
+
40
+ def children_path(base: str, path: str | None) -> str:
41
+ if not path:
42
+ return f"{base}/root/children"
43
+ return f"{base}/root:/{odata.drive_path(path.lstrip('/'))}:/children"
44
+
45
+
46
+ def list_children(
47
+ client: GraphClient, base: str, path: str | None, *, limit: int | None, all_: bool
48
+ ) -> PageResult:
49
+ return client.paginate(
50
+ children_path(base, path),
51
+ params={"$select": ITEM_SELECT, "$orderby": "name"},
52
+ limit=limit,
53
+ all_=all_,
54
+ cap=CAP_LS,
55
+ page_size=PAGE_LS,
56
+ )
57
+
58
+
59
+ def search_items(
60
+ client: GraphClient, base: str, q: str, *, limit: int | None, shared: bool = False
61
+ ) -> PageResult:
62
+ prefix = base if shared else f"{base}/root"
63
+ path = f"{prefix}/search(q='{odata.odata_str(q)}')"
64
+ return client.paginate(
65
+ path,
66
+ params={"$select": ITEM_SELECT},
67
+ limit=limit,
68
+ all_=False,
69
+ cap=CAP_LS,
70
+ page_size=PAGE_LS,
71
+ )
72
+
73
+
74
+ def get_item(client: GraphClient, base: str, ref: str) -> dict:
75
+ return client.get(item_ref(base, ref), params={"$select": ITEM_SELECT})
76
+
77
+
78
+ def resolve_item_id(client: GraphClient, base: str, ref: str) -> str:
79
+ value, forced = resolve.split_id_prefix(ref)
80
+ if resolve.looks_like_id(ref, "drive_item"):
81
+ return value
82
+ return get_item(client, base, ref)["id"]
83
+
84
+
85
+ def _action_path(base: str, ref: str, action: str) -> str:
86
+ """`item_ref(...)` plus an action segment, colon-fenced correctly for each ref form.
87
+
88
+ An id-form ref (`{base}/items/{id}`) takes the action as a plain path segment
89
+ (`/items/{id}/action`); a path-form ref (`{base}/root:/{path}`) needs the path's closing
90
+ colon before the action segment (`root:/{path}:/action`) or Graph 400s.
91
+ """
92
+ sep = "/" if resolve.looks_like_id(ref, "drive_item") else ":/"
93
+ return item_ref(base, ref) + sep + action
94
+
95
+
96
+ def _default_name(item: dict) -> Path:
97
+ """Where a download lands with no `--output`: the item's basename, never a path Graph chose."""
98
+ return Path(Path(item.get("name") or "").name or item["id"])
99
+
100
+
101
+ def download_item(
102
+ client: GraphClient, base: str, ref: str, dest: Path | None
103
+ ) -> tuple[dict, DownloadResult]:
104
+ item = get_item(client, base, ref)
105
+ result = client.download(_action_path(base, ref, "content"), dest or _default_name(item))
106
+ return item, result
107
+
108
+
109
+ def _dest(file: Path, dest: str | None) -> str:
110
+ """`--dest` resolution (spec §8.11 `upload`): default `/<basename>`; trailing `/` = folder."""
111
+ if dest is None:
112
+ return f"/{file.name}"
113
+ if dest.endswith("/"):
114
+ return f"{dest}{file.name}"
115
+ return dest
116
+
117
+
118
+ def plan_upload(
119
+ client: GraphClient, base: str, file: Path, dest: str | None, conflict: str
120
+ ) -> Plan:
121
+ if conflict not in CONFLICT_BEHAVIORS:
122
+ raise UsageError(
123
+ "USAGE",
124
+ f"--conflict must be one of {', '.join(sorted(CONFLICT_BEHAVIORS))} (got {conflict!r})",
125
+ )
126
+ dest_path = _dest(file, dest)
127
+ encoded = odata.drive_path(dest_path.lstrip("/"))
128
+ size = file.stat().st_size
129
+ if size < config.DRIVE_SIMPLE_UPLOAD:
130
+ url = client.url(
131
+ f"{base}/root:/{encoded}:/content?@microsoft.graph.conflictBehavior={conflict}"
132
+ )
133
+ content_type = guess_type(file.name)[0] or "application/octet-stream"
134
+ return [PlannedRequest("PUT", url, {"Content-Type": content_type}, None, file=file)]
135
+ url = client.url(f"{base}/root:/{encoded}:/createUploadSession")
136
+ name = dest_path.rsplit("/", 1)[-1]
137
+ body = {"item": {"@microsoft.graph.conflictBehavior": conflict, "name": name}}
138
+ note = f"upload {size} bytes in {ceil(size / config.CHUNK_DRIVE)} chunks"
139
+ return [
140
+ PlannedRequest(
141
+ "POST",
142
+ url,
143
+ dict(JSON_HEADERS),
144
+ body,
145
+ file=file,
146
+ chunk_size=config.CHUNK_DRIVE,
147
+ note=note,
148
+ )
149
+ ]
150
+
151
+
152
+ def _run(client: GraphClient, plan: Plan) -> Any:
153
+ """Execute every step of `plan` in order, returning the last step's result."""
154
+ result: Any = None
155
+ for step in plan:
156
+ result = client.execute(step)
157
+ return result
158
+
159
+
160
+ def run_upload(client: GraphClient, base: str, file: Path, dest: str | None, conflict: str) -> dict:
161
+ return _run(client, plan_upload(client, base, file, dest, conflict))
162
+
163
+
164
+ def plan_mkdir(client: GraphClient, base: str, path: str) -> Plan:
165
+ clean = path.strip("/")
166
+ if "/" in clean:
167
+ parent, name = clean.rsplit("/", 1)
168
+ url = client.url(f"{base}/root:/{odata.drive_path(parent)}:/children")
169
+ else:
170
+ name = clean
171
+ url = client.url(f"{base}/root/children")
172
+ body = {"name": name, "folder": {}, "@microsoft.graph.conflictBehavior": "fail"}
173
+ return [PlannedRequest("POST", url, dict(JSON_HEADERS), body)]
174
+
175
+
176
+ def run_mkdir(client: GraphClient, base: str, path: str) -> dict:
177
+ return _run(client, plan_mkdir(client, base, path))
178
+
179
+
180
+ def _resolve_folder_id(client: GraphClient, base: str, to: str) -> str:
181
+ value, forced = resolve.split_id_prefix(to)
182
+ if forced:
183
+ return value
184
+ return get_item(client, base, to)["id"]
185
+
186
+
187
+ def plan_move(client: GraphClient, base: str, ref: str, to: str, name: str | None) -> Plan:
188
+ folder_id = _resolve_folder_id(client, base, to)
189
+ body: dict[str, Any] = {"parentReference": {"id": folder_id}}
190
+ if name is not None:
191
+ body["name"] = name
192
+ url = client.url(item_ref(base, ref))
193
+ return [PlannedRequest("PATCH", url, dict(JSON_HEADERS), body)]
194
+
195
+
196
+ def run_move(client: GraphClient, base: str, ref: str, to: str, name: str | None) -> dict:
197
+ return _run(client, plan_move(client, base, ref, to, name))
198
+
199
+
200
+ def plan_rename(client: GraphClient, base: str, ref: str, name: str) -> Plan:
201
+ url = client.url(item_ref(base, ref))
202
+ return [PlannedRequest("PATCH", url, dict(JSON_HEADERS), {"name": name})]
203
+
204
+
205
+ def run_rename(client: GraphClient, base: str, ref: str, name: str) -> dict:
206
+ return _run(client, plan_rename(client, base, ref, name))
207
+
208
+
209
+ def plan_delete(client: GraphClient, base: str, ref: str) -> Plan:
210
+ url = client.url(item_ref(base, ref))
211
+ return [PlannedRequest("DELETE", url, {}, None, expect="none")]
212
+
213
+
214
+ def run_delete(client: GraphClient, base: str, ref: str) -> dict | None:
215
+ return _run(client, plan_delete(client, base, ref))
216
+
217
+
218
+ def plan_share(
219
+ client: GraphClient,
220
+ base: str,
221
+ ref: str,
222
+ *,
223
+ link_type: str,
224
+ scope: str,
225
+ expires: datetime | None,
226
+ ) -> Plan:
227
+ body: dict[str, Any] = {"type": link_type, "scope": scope}
228
+ if expires is not None:
229
+ body["expirationDateTime"] = to_iso_offset(expires)
230
+ url = client.url(_action_path(base, ref, "createLink"))
231
+ return [PlannedRequest("POST", url, dict(JSON_HEADERS), body)]
232
+
233
+
234
+ def run_share(
235
+ client: GraphClient,
236
+ base: str,
237
+ ref: str,
238
+ *,
239
+ link_type: str,
240
+ scope: str,
241
+ expires: datetime | None,
242
+ ) -> dict:
243
+ return _run(
244
+ client, plan_share(client, base, ref, link_type=link_type, scope=scope, expires=expires)
245
+ )
246
+
247
+
248
+ def get_shared_item(client: GraphClient, url: str) -> dict:
249
+ return client.get(f"/shares/{odata.share_id(url)}/driveItem")
250
+
251
+
252
+ def download_shared_item(
253
+ client: GraphClient, url: str, dest: Path | None
254
+ ) -> tuple[dict, DownloadResult]:
255
+ item = get_shared_item(client, url)
256
+ result = client.download(
257
+ f"/shares/{odata.share_id(url)}/driveItem/content", dest or _default_name(item)
258
+ )
259
+ return item, result
260
+
261
+
262
+ def item_columns(tz: str) -> list[Column]:
263
+ return [
264
+ Column("type", lambda i: "d" if "folder" in i else "f"),
265
+ Column("id", "id"),
266
+ Column("size", lambda i: fmt_size(i.get("size"))),
267
+ Column("modified", lambda i: fmt_dt(i.get("lastModifiedDateTime"), tz)),
268
+ Column("name", "name"),
269
+ ]
@@ -0,0 +1,55 @@
1
+ """Graph operations for Microsoft 365 groups (spec §8.16).
2
+
3
+ Pure: client and parameters in, Graph dicts / PageResult out.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from mgraphctl import odata
9
+ from mgraphctl.graph.users import GROUPS_PATH, list_unified_groups
10
+ from mgraphctl.http import GraphClient, PageResult
11
+ from mgraphctl.resolve import looks_like_id, pick_unique
12
+
13
+ LIST_SELECT = "id,displayName,mail,groupTypes,description"
14
+ MEMBERS_SELECT = "id,displayName,userPrincipalName,mail,jobTitle"
15
+
16
+ PAGE = 100
17
+ CAP = 999
18
+
19
+
20
+ def list_groups(client: GraphClient, *, unified: bool, limit: int, all_: bool) -> PageResult:
21
+ """Groups the signed-in user belongs to; `--unified` narrows to Microsoft 365 groups."""
22
+ if unified:
23
+ items = list_unified_groups(client)
24
+ bound = CAP if all_ else min(limit, CAP)
25
+ return PageResult(items=items[:bound], truncated=len(items) > bound, pages=1)
26
+ return client.paginate(
27
+ GROUPS_PATH,
28
+ params={"$select": LIST_SELECT},
29
+ limit=limit,
30
+ all_=all_,
31
+ cap=CAP,
32
+ page_size=PAGE,
33
+ )
34
+
35
+
36
+ def list_members(client: GraphClient, group_id: str, *, limit: int, all_: bool) -> PageResult:
37
+ path = f"{odata.p('groups', group_id)}/members"
38
+ return client.paginate(
39
+ path, params={"$select": MEMBERS_SELECT}, limit=limit, all_=all_, cap=CAP, page_size=PAGE
40
+ )
41
+
42
+
43
+ def resolve_group(client: GraphClient, value: str) -> dict:
44
+ """Turn a group name or id into a group object (at least `id`); by-name via `memberOf`."""
45
+ if looks_like_id(value, "group"):
46
+ return {"id": value}
47
+ page = client.paginate(
48
+ GROUPS_PATH,
49
+ params={"$select": LIST_SELECT},
50
+ limit=None,
51
+ all_=True,
52
+ cap=CAP,
53
+ page_size=PAGE,
54
+ )
55
+ return pick_unique(page.items, "displayName", value, what="group")