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.
- mgraphctl/__init__.py +3 -0
- mgraphctl/__main__.py +3 -0
- mgraphctl/auth.py +395 -0
- mgraphctl/cli.py +259 -0
- mgraphctl/commands/__init__.py +71 -0
- mgraphctl/commands/api.py +164 -0
- mgraphctl/commands/calendar.py +491 -0
- mgraphctl/commands/chats.py +329 -0
- mgraphctl/commands/config_cmd.py +140 -0
- mgraphctl/commands/groups.py +65 -0
- mgraphctl/commands/mail.py +705 -0
- mgraphctl/commands/mailbox.py +176 -0
- mgraphctl/commands/meetings.py +294 -0
- mgraphctl/commands/onedrive.py +351 -0
- mgraphctl/commands/onenote.py +196 -0
- mgraphctl/commands/org.py +102 -0
- mgraphctl/commands/people.py +201 -0
- mgraphctl/commands/planner.py +268 -0
- mgraphctl/commands/presence.py +102 -0
- mgraphctl/commands/search.py +75 -0
- mgraphctl/commands/sharepoint.py +304 -0
- mgraphctl/commands/teams.py +235 -0
- mgraphctl/commands/todo.py +253 -0
- mgraphctl/commands/top.py +284 -0
- mgraphctl/config.py +385 -0
- mgraphctl/errors.py +276 -0
- mgraphctl/fixtures.py +149 -0
- mgraphctl/graph/__init__.py +1 -0
- mgraphctl/graph/calendar.py +313 -0
- mgraphctl/graph/chats.py +447 -0
- mgraphctl/graph/files.py +269 -0
- mgraphctl/graph/groups.py +55 -0
- mgraphctl/graph/mail.py +663 -0
- mgraphctl/graph/mailbox.py +89 -0
- mgraphctl/graph/meetings.py +269 -0
- mgraphctl/graph/onedrive.py +35 -0
- mgraphctl/graph/onenote.py +138 -0
- mgraphctl/graph/org.py +86 -0
- mgraphctl/graph/people.py +83 -0
- mgraphctl/graph/planner.py +415 -0
- mgraphctl/graph/presence.py +89 -0
- mgraphctl/graph/search.py +178 -0
- mgraphctl/graph/sharepoint.py +294 -0
- mgraphctl/graph/teams.py +240 -0
- mgraphctl/graph/todo.py +199 -0
- mgraphctl/graph/users.py +85 -0
- mgraphctl/html.py +127 -0
- mgraphctl/http.py +843 -0
- mgraphctl/odata.py +72 -0
- mgraphctl/render.py +627 -0
- mgraphctl/resolve.py +111 -0
- mgraphctl/token_store.py +196 -0
- mgraphctl-0.1.0.dist-info/METADATA +226 -0
- mgraphctl-0.1.0.dist-info/RECORD +57 -0
- mgraphctl-0.1.0.dist-info/WHEEL +4 -0
- mgraphctl-0.1.0.dist-info/entry_points.txt +3 -0
- mgraphctl-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""OneDrive files: browse, search, up/download, organise (spec §8.11).
|
|
2
|
+
|
|
3
|
+
Every verb targets the signed-in user's own drive unless `--drive DRIVE_ID` names another one
|
|
4
|
+
(`shared-with-me`, `recent` and `link` are always the caller's own drive/shares, so they have no
|
|
5
|
+
`--drive` option). Item and folder metadata operations delegate to `mgraphctl.graph.files`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated, Literal
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from mgraphctl.cli import (
|
|
14
|
+
AllFlag,
|
|
15
|
+
DryRunFlag,
|
|
16
|
+
JsonFlag,
|
|
17
|
+
LimitOpt,
|
|
18
|
+
graph_command,
|
|
19
|
+
make_noun_app,
|
|
20
|
+
page_bounds,
|
|
21
|
+
)
|
|
22
|
+
from mgraphctl.graph import files, onedrive
|
|
23
|
+
from mgraphctl.http import GraphClient
|
|
24
|
+
from mgraphctl.render import (
|
|
25
|
+
Column,
|
|
26
|
+
DryRunResult,
|
|
27
|
+
FileResult,
|
|
28
|
+
ListResult,
|
|
29
|
+
ObjectResult,
|
|
30
|
+
WriteResult,
|
|
31
|
+
dig,
|
|
32
|
+
fmt_dt,
|
|
33
|
+
fmt_size,
|
|
34
|
+
note,
|
|
35
|
+
parse_dt,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
app = make_noun_app("OneDrive files.")
|
|
39
|
+
|
|
40
|
+
DriveOpt = Annotated[str | None, typer.Option("--drive", help="Target another drive by id.")]
|
|
41
|
+
RefArg = Annotated[str, typer.Argument(metavar="ID|PATH", help="Item id, 'id:ID', or a path.")]
|
|
42
|
+
|
|
43
|
+
SHARED_WITH_ME_NOTE = (
|
|
44
|
+
"note: /me/drive/sharedWithMe is deprecated by Microsoft and returns nothing after "
|
|
45
|
+
"November 2026"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _item_fields(tz: str) -> list[tuple[str, object]]:
|
|
50
|
+
"""`Label : value` fields for `get`/`link` (an `ObjectResult` view of one drive item)."""
|
|
51
|
+
return [
|
|
52
|
+
("Name", "name"),
|
|
53
|
+
("Id", "id"),
|
|
54
|
+
("Type", lambda i: "folder" if "folder" in i else "file"),
|
|
55
|
+
("Size", lambda i: fmt_size(i.get("size"))),
|
|
56
|
+
("Modified", lambda i: fmt_dt(i.get("lastModifiedDateTime"), tz)),
|
|
57
|
+
("Web URL", "webUrl"),
|
|
58
|
+
("Parent", lambda i: dig(i, "parentReference.path")),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# --------------------------------------------------------------------------- browsing
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("ls")
|
|
66
|
+
@graph_command(scopes=["Files.Read"])
|
|
67
|
+
def ls(
|
|
68
|
+
client: GraphClient,
|
|
69
|
+
path: Annotated[str | None, typer.Argument(metavar="PATH")] = None,
|
|
70
|
+
drive: DriveOpt = None,
|
|
71
|
+
limit: LimitOpt = None,
|
|
72
|
+
all_: AllFlag = False,
|
|
73
|
+
json_: JsonFlag = False,
|
|
74
|
+
):
|
|
75
|
+
"""List the files and folders in a drive (default: the drive root)."""
|
|
76
|
+
limit, all_ = page_bounds(limit, all_, default=50)
|
|
77
|
+
base = onedrive.base_for(drive)
|
|
78
|
+
page = files.list_children(client, base, path, limit=limit, all_=all_)
|
|
79
|
+
return ListResult(
|
|
80
|
+
items=page.items,
|
|
81
|
+
page=page,
|
|
82
|
+
hit_cap=files.CAP_LS if all_ else None,
|
|
83
|
+
columns=files.item_columns(client.tz),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("search")
|
|
88
|
+
@graph_command(scopes=["Files.Read"])
|
|
89
|
+
def search(
|
|
90
|
+
client: GraphClient,
|
|
91
|
+
q: Annotated[str, typer.Argument(metavar="Q")],
|
|
92
|
+
drive: DriveOpt = None,
|
|
93
|
+
shared: Annotated[
|
|
94
|
+
bool, typer.Option("--shared", help="Search everything shared with you too.")
|
|
95
|
+
] = False,
|
|
96
|
+
limit: LimitOpt = None,
|
|
97
|
+
json_: JsonFlag = False,
|
|
98
|
+
):
|
|
99
|
+
"""Search files and folders by name or content."""
|
|
100
|
+
base = onedrive.base_for(drive)
|
|
101
|
+
bound = limit if limit is not None else 50
|
|
102
|
+
page = files.search_items(client, base, q, limit=bound, shared=shared)
|
|
103
|
+
return ListResult(
|
|
104
|
+
items=page.items,
|
|
105
|
+
page=page,
|
|
106
|
+
supports_all=False,
|
|
107
|
+
columns=files.item_columns(client.tz),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("get")
|
|
112
|
+
@graph_command(scopes=["Files.Read"])
|
|
113
|
+
def get(
|
|
114
|
+
client: GraphClient,
|
|
115
|
+
ref: RefArg,
|
|
116
|
+
drive: DriveOpt = None,
|
|
117
|
+
json_: JsonFlag = False,
|
|
118
|
+
):
|
|
119
|
+
"""Show one file or folder's metadata."""
|
|
120
|
+
base = onedrive.base_for(drive)
|
|
121
|
+
item = files.get_item(client, base, ref)
|
|
122
|
+
return ObjectResult(obj=item, fields=_item_fields(client.tz))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("download")
|
|
126
|
+
@graph_command(scopes=["Files.Read"])
|
|
127
|
+
def download(
|
|
128
|
+
client: GraphClient,
|
|
129
|
+
ref: RefArg,
|
|
130
|
+
drive: DriveOpt = None,
|
|
131
|
+
output: Annotated[
|
|
132
|
+
Path | None, typer.Option("--output", help="Destination file (default: the item name).")
|
|
133
|
+
] = None,
|
|
134
|
+
json_: JsonFlag = False,
|
|
135
|
+
):
|
|
136
|
+
"""Download a file's contents."""
|
|
137
|
+
base = onedrive.base_for(drive)
|
|
138
|
+
item, got = files.download_item(client, base, ref, output)
|
|
139
|
+
return FileResult(
|
|
140
|
+
path=got.path,
|
|
141
|
+
bytes=got.bytes,
|
|
142
|
+
meta=dict(contentType=got.content_type),
|
|
143
|
+
message=f"Downloaded {item['name']} ({fmt_size(got.bytes)}) to {got.path}",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# --------------------------------------------------------------------------- writes
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@app.command("upload")
|
|
151
|
+
@graph_command(scopes=["Files.ReadWrite"])
|
|
152
|
+
def upload(
|
|
153
|
+
client: GraphClient,
|
|
154
|
+
file: Annotated[
|
|
155
|
+
Path,
|
|
156
|
+
typer.Argument(metavar="FILE", help="Local file to upload.", exists=True, dir_okay=False),
|
|
157
|
+
],
|
|
158
|
+
drive: DriveOpt = None,
|
|
159
|
+
dest: Annotated[
|
|
160
|
+
str | None,
|
|
161
|
+
typer.Option(
|
|
162
|
+
"--dest", help="Destination path (default: /<basename>; trailing / = folder)."
|
|
163
|
+
),
|
|
164
|
+
] = None,
|
|
165
|
+
conflict: Annotated[
|
|
166
|
+
Literal["rename", "replace", "fail"], typer.Option("--conflict", help="On a name clash.")
|
|
167
|
+
] = "replace",
|
|
168
|
+
dry_run: DryRunFlag = False,
|
|
169
|
+
json_: JsonFlag = False,
|
|
170
|
+
):
|
|
171
|
+
"""Upload a local file (simple PUT under 4 MiB, an upload session above it)."""
|
|
172
|
+
base = onedrive.base_for(drive)
|
|
173
|
+
if dry_run:
|
|
174
|
+
return DryRunResult(files.plan_upload(client, base, file, dest, conflict))
|
|
175
|
+
item = files.run_upload(client, base, file, dest, conflict)
|
|
176
|
+
return WriteResult(obj=item, message=f"Uploaded {file.name}.")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.command("mkdir")
|
|
180
|
+
@graph_command(scopes=["Files.ReadWrite"])
|
|
181
|
+
def mkdir(
|
|
182
|
+
client: GraphClient,
|
|
183
|
+
path: Annotated[str, typer.Argument(metavar="PATH")],
|
|
184
|
+
drive: DriveOpt = None,
|
|
185
|
+
dry_run: DryRunFlag = False,
|
|
186
|
+
json_: JsonFlag = False,
|
|
187
|
+
):
|
|
188
|
+
"""Create a folder."""
|
|
189
|
+
base = onedrive.base_for(drive)
|
|
190
|
+
if dry_run:
|
|
191
|
+
return DryRunResult(files.plan_mkdir(client, base, path))
|
|
192
|
+
item = files.run_mkdir(client, base, path)
|
|
193
|
+
return WriteResult(obj=item, message=f"Created {item.get('name', path)}.")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.command("move")
|
|
197
|
+
@graph_command(scopes=["Files.ReadWrite"])
|
|
198
|
+
def move(
|
|
199
|
+
client: GraphClient,
|
|
200
|
+
ref: RefArg,
|
|
201
|
+
to: Annotated[str, typer.Option("--to", help="Destination folder path, or 'id:ID'.")] = ...,
|
|
202
|
+
drive: DriveOpt = None,
|
|
203
|
+
name: Annotated[str | None, typer.Option("--name", help="Rename while moving.")] = None,
|
|
204
|
+
dry_run: DryRunFlag = False,
|
|
205
|
+
json_: JsonFlag = False,
|
|
206
|
+
):
|
|
207
|
+
"""Move (and optionally rename) a file or folder."""
|
|
208
|
+
base = onedrive.base_for(drive)
|
|
209
|
+
if dry_run:
|
|
210
|
+
return DryRunResult(files.plan_move(client, base, ref, to, name))
|
|
211
|
+
item = files.run_move(client, base, ref, to, name)
|
|
212
|
+
return WriteResult(obj=item, message=f"Moved {ref} to {to}.")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command("rename")
|
|
216
|
+
@graph_command(scopes=["Files.ReadWrite"])
|
|
217
|
+
def rename(
|
|
218
|
+
client: GraphClient,
|
|
219
|
+
ref: RefArg,
|
|
220
|
+
name: Annotated[str, typer.Argument(metavar="NAME")],
|
|
221
|
+
drive: DriveOpt = None,
|
|
222
|
+
dry_run: DryRunFlag = False,
|
|
223
|
+
json_: JsonFlag = False,
|
|
224
|
+
):
|
|
225
|
+
"""Rename a file or folder."""
|
|
226
|
+
base = onedrive.base_for(drive)
|
|
227
|
+
if dry_run:
|
|
228
|
+
return DryRunResult(files.plan_rename(client, base, ref, name))
|
|
229
|
+
item = files.run_rename(client, base, ref, name)
|
|
230
|
+
return WriteResult(obj=item, message=f"Renamed to {name}.")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.command("delete")
|
|
234
|
+
@graph_command(scopes=["Files.ReadWrite"])
|
|
235
|
+
def delete(
|
|
236
|
+
client: GraphClient,
|
|
237
|
+
ref: RefArg,
|
|
238
|
+
drive: DriveOpt = None,
|
|
239
|
+
dry_run: DryRunFlag = False,
|
|
240
|
+
json_: JsonFlag = False,
|
|
241
|
+
):
|
|
242
|
+
"""Delete a file or folder (moves it to the recycle bin)."""
|
|
243
|
+
base = onedrive.base_for(drive)
|
|
244
|
+
if dry_run:
|
|
245
|
+
return DryRunResult(files.plan_delete(client, base, ref))
|
|
246
|
+
item_id = files.resolve_item_id(client, base, ref)
|
|
247
|
+
files.run_delete(client, base, ref)
|
|
248
|
+
return WriteResult(obj={"status": "deleted", "id": item_id}, message=f"Deleted {item_id}.")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command("share")
|
|
252
|
+
@graph_command(scopes=["Files.ReadWrite"])
|
|
253
|
+
def share(
|
|
254
|
+
client: GraphClient,
|
|
255
|
+
ref: RefArg,
|
|
256
|
+
drive: DriveOpt = None,
|
|
257
|
+
link_type: Annotated[
|
|
258
|
+
Literal["view", "edit"], typer.Option("--type", help="Kind of link to create.")
|
|
259
|
+
] = "view",
|
|
260
|
+
scope: Annotated[
|
|
261
|
+
Literal["organization", "anonymous"], typer.Option("--scope", help="Who can use it.")
|
|
262
|
+
] = "organization",
|
|
263
|
+
expires: Annotated[
|
|
264
|
+
str | None, typer.Option("--expires", help="When the link stops working.")
|
|
265
|
+
] = None,
|
|
266
|
+
dry_run: DryRunFlag = False,
|
|
267
|
+
json_: JsonFlag = False,
|
|
268
|
+
):
|
|
269
|
+
"""Create a sharing link."""
|
|
270
|
+
base = onedrive.base_for(drive)
|
|
271
|
+
expires_dt = parse_dt(expires, client.tz) if expires else None
|
|
272
|
+
if dry_run:
|
|
273
|
+
return DryRunResult(
|
|
274
|
+
files.plan_share(
|
|
275
|
+
client, base, ref, link_type=link_type, scope=scope, expires=expires_dt
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
link = files.run_share(client, base, ref, link_type=link_type, scope=scope, expires=expires_dt)
|
|
279
|
+
return WriteResult(obj=link, message=dig(link, "link.webUrl") or "")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# --------------------------------------------------------------------------- own-drive listings
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.command("shared-with-me")
|
|
286
|
+
@graph_command(scopes=["Files.Read.All|Sites.Read.All"])
|
|
287
|
+
def shared_with_me(
|
|
288
|
+
client: GraphClient,
|
|
289
|
+
limit: LimitOpt = None,
|
|
290
|
+
json_: JsonFlag = False,
|
|
291
|
+
):
|
|
292
|
+
"""List items other people have shared with you."""
|
|
293
|
+
note(SHARED_WITH_ME_NOTE)
|
|
294
|
+
bound = limit if limit is not None else 50
|
|
295
|
+
page = onedrive.shared_with_me(client, limit=bound)
|
|
296
|
+
tz = client.tz
|
|
297
|
+
columns = [
|
|
298
|
+
Column("id", "id"),
|
|
299
|
+
Column("name", "name"),
|
|
300
|
+
Column("size", lambda i: fmt_size(i.get("size"))),
|
|
301
|
+
Column("modified", lambda i: fmt_dt(i.get("lastModifiedDateTime"), tz)),
|
|
302
|
+
Column("sharedDriveId", lambda i: dig(i, "remoteItem.parentReference.driveId") or ""),
|
|
303
|
+
Column("sharedItemId", lambda i: dig(i, "remoteItem.id") or ""),
|
|
304
|
+
]
|
|
305
|
+
return ListResult(items=page.items, page=page, supports_all=False, columns=columns)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@app.command("recent")
|
|
309
|
+
@graph_command(scopes=["Files.Read"])
|
|
310
|
+
def recent(
|
|
311
|
+
client: GraphClient,
|
|
312
|
+
limit: LimitOpt = None,
|
|
313
|
+
json_: JsonFlag = False,
|
|
314
|
+
):
|
|
315
|
+
"""List recently used files."""
|
|
316
|
+
bound = limit if limit is not None else 20
|
|
317
|
+
page = onedrive.recent(client, limit=bound)
|
|
318
|
+
return ListResult(
|
|
319
|
+
items=page.items,
|
|
320
|
+
page=page,
|
|
321
|
+
supports_all=False,
|
|
322
|
+
columns=files.item_columns(client.tz),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@app.command("link")
|
|
327
|
+
@graph_command(scopes=["Files.Read"])
|
|
328
|
+
def link(
|
|
329
|
+
client: GraphClient,
|
|
330
|
+
url: Annotated[
|
|
331
|
+
str, typer.Argument(metavar="URL", help="A OneDrive or SharePoint sharing link.")
|
|
332
|
+
],
|
|
333
|
+
download: Annotated[
|
|
334
|
+
bool, typer.Option("--download", help="Download the item's content.")
|
|
335
|
+
] = False,
|
|
336
|
+
output: Annotated[
|
|
337
|
+
Path | None, typer.Option("--output", help="Destination file for --download.")
|
|
338
|
+
] = None,
|
|
339
|
+
json_: JsonFlag = False,
|
|
340
|
+
):
|
|
341
|
+
"""Resolve a sharing link to the item it points at."""
|
|
342
|
+
if download:
|
|
343
|
+
item, got = files.download_shared_item(client, url, output)
|
|
344
|
+
return FileResult(
|
|
345
|
+
path=got.path,
|
|
346
|
+
bytes=got.bytes,
|
|
347
|
+
meta=dict(contentType=got.content_type),
|
|
348
|
+
message=f"Downloaded {item['name']} ({fmt_size(got.bytes)}) to {got.path}",
|
|
349
|
+
)
|
|
350
|
+
item = files.get_shared_item(client, url)
|
|
351
|
+
return ObjectResult(obj=item, fields=_item_fields(client.tz))
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""OneNote notebooks, sections and pages (spec §8.13)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from mgraphctl.cli import (
|
|
9
|
+
AllFlag,
|
|
10
|
+
DryRunFlag,
|
|
11
|
+
JsonFlag,
|
|
12
|
+
LimitOpt,
|
|
13
|
+
graph_command,
|
|
14
|
+
make_noun_app,
|
|
15
|
+
page_bounds,
|
|
16
|
+
read_body,
|
|
17
|
+
)
|
|
18
|
+
from mgraphctl.graph import onenote
|
|
19
|
+
from mgraphctl.html import to_markdown
|
|
20
|
+
from mgraphctl.http import GraphClient
|
|
21
|
+
from mgraphctl.render import (
|
|
22
|
+
Column,
|
|
23
|
+
DryRunResult,
|
|
24
|
+
FileResult,
|
|
25
|
+
ListResult,
|
|
26
|
+
TextResult,
|
|
27
|
+
WriteResult,
|
|
28
|
+
fmt_dt,
|
|
29
|
+
fmt_size,
|
|
30
|
+
truncate,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
DEFAULT_LIMIT = 50
|
|
34
|
+
|
|
35
|
+
app = make_noun_app("OneNote notebooks and pages.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("notebooks")
|
|
39
|
+
@graph_command(scopes=["Notes.Read"])
|
|
40
|
+
def notebooks(
|
|
41
|
+
client: GraphClient,
|
|
42
|
+
limit: LimitOpt = None,
|
|
43
|
+
json_: JsonFlag = False,
|
|
44
|
+
):
|
|
45
|
+
"""List OneNote notebooks."""
|
|
46
|
+
bound = limit if limit is not None else DEFAULT_LIMIT
|
|
47
|
+
page = onenote.list_notebooks(client, limit=bound)
|
|
48
|
+
tz = client.tz
|
|
49
|
+
return ListResult(
|
|
50
|
+
items=page.items,
|
|
51
|
+
page=page,
|
|
52
|
+
supports_all=False,
|
|
53
|
+
columns=[
|
|
54
|
+
Column("id", "id"),
|
|
55
|
+
Column("name", "displayName"),
|
|
56
|
+
Column("modified", lambda n: fmt_dt(n.get("lastModifiedDateTime"), tz)),
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("sections")
|
|
62
|
+
@graph_command(scopes=["Notes.Read"])
|
|
63
|
+
def sections(
|
|
64
|
+
client: GraphClient,
|
|
65
|
+
notebook: Annotated[str | None, typer.Argument(metavar="NOTEBOOK")] = None,
|
|
66
|
+
limit: LimitOpt = None,
|
|
67
|
+
json_: JsonFlag = False,
|
|
68
|
+
):
|
|
69
|
+
"""List OneNote sections, across all notebooks or within one."""
|
|
70
|
+
bound = limit if limit is not None else DEFAULT_LIMIT
|
|
71
|
+
notebook_id = onenote.resolve_notebook(client, notebook) if notebook is not None else None
|
|
72
|
+
page = onenote.list_sections(client, notebook_id, limit=bound)
|
|
73
|
+
tz = client.tz
|
|
74
|
+
return ListResult(
|
|
75
|
+
items=page.items,
|
|
76
|
+
page=page,
|
|
77
|
+
supports_all=False,
|
|
78
|
+
columns=[
|
|
79
|
+
Column("id", "id"),
|
|
80
|
+
Column("name", "displayName"),
|
|
81
|
+
Column("modified", lambda s: fmt_dt(s.get("lastModifiedDateTime"), tz)),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("pages")
|
|
87
|
+
@graph_command(scopes=["Notes.Read"])
|
|
88
|
+
def pages(
|
|
89
|
+
client: GraphClient,
|
|
90
|
+
section: Annotated[str, typer.Argument(metavar="SECTION")],
|
|
91
|
+
limit: LimitOpt = None,
|
|
92
|
+
all_: AllFlag = False,
|
|
93
|
+
json_: JsonFlag = False,
|
|
94
|
+
):
|
|
95
|
+
"""List the pages in a section."""
|
|
96
|
+
bound, all_pages = page_bounds(limit, all_, default=DEFAULT_LIMIT)
|
|
97
|
+
section_id = onenote.resolve_section(client, section)
|
|
98
|
+
page = onenote.list_pages(client, section_id, limit=bound, all_=all_pages)
|
|
99
|
+
tz = client.tz
|
|
100
|
+
return ListResult(
|
|
101
|
+
items=page.items,
|
|
102
|
+
page=page,
|
|
103
|
+
hit_cap=onenote.CAP_LIST if all_pages else None,
|
|
104
|
+
columns=[
|
|
105
|
+
Column("id", "id"),
|
|
106
|
+
Column("modified", lambda pg: fmt_dt(pg.get("lastModifiedDateTime"), tz)),
|
|
107
|
+
Column("title", lambda pg: truncate(pg.get("title"))),
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.command("read")
|
|
113
|
+
@graph_command(scopes=["Notes.Read"])
|
|
114
|
+
def read(
|
|
115
|
+
client: GraphClient,
|
|
116
|
+
page: Annotated[str, typer.Argument(metavar="PAGE")],
|
|
117
|
+
html_: Annotated[bool, typer.Option("--html", help="Print the page's raw HTML.")] = False,
|
|
118
|
+
output: Annotated[
|
|
119
|
+
Path | None, typer.Option("--output", help="Write to this file instead of stdout.")
|
|
120
|
+
] = None,
|
|
121
|
+
json_: JsonFlag = False,
|
|
122
|
+
):
|
|
123
|
+
"""Read a OneNote page as Markdown (or raw HTML with --html)."""
|
|
124
|
+
page_id = onenote.resolve_page(client, page)
|
|
125
|
+
meta, html_body = onenote.get_page(client, page_id)
|
|
126
|
+
markdown = to_markdown(html_body, mode="onenote")
|
|
127
|
+
doc = {
|
|
128
|
+
"id": meta.get("id"),
|
|
129
|
+
"title": meta.get("title"),
|
|
130
|
+
"html": html_body,
|
|
131
|
+
"markdown": markdown,
|
|
132
|
+
}
|
|
133
|
+
if output is not None:
|
|
134
|
+
content = html_body if html_ else markdown
|
|
135
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
data = content.encode("utf-8")
|
|
137
|
+
output.write_bytes(data)
|
|
138
|
+
return FileResult(
|
|
139
|
+
path=output,
|
|
140
|
+
bytes=len(data),
|
|
141
|
+
meta=dict(id=doc["id"], title=doc["title"]),
|
|
142
|
+
message=f"Wrote {fmt_size(len(data))} to {output}",
|
|
143
|
+
)
|
|
144
|
+
if html_:
|
|
145
|
+
return TextResult(text=html_body, json_obj=doc)
|
|
146
|
+
return TextResult(text=markdown, json_obj=doc)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command("create")
|
|
150
|
+
@graph_command(scopes=["Notes.ReadWrite"])
|
|
151
|
+
def create(
|
|
152
|
+
client: GraphClient,
|
|
153
|
+
section: Annotated[str, typer.Option("--section", help="Section name or id.")],
|
|
154
|
+
title: Annotated[str, typer.Option("--title", help="Page title.")],
|
|
155
|
+
body: Annotated[str | None, typer.Option("--body", help="Page body text.")] = None,
|
|
156
|
+
body_file: Annotated[
|
|
157
|
+
str | None, typer.Option("--body-file", help="File, or - for stdin.")
|
|
158
|
+
] = None,
|
|
159
|
+
html_: Annotated[
|
|
160
|
+
bool, typer.Option("--html", help="Treat --body/--body-file as raw HTML, unescaped.")
|
|
161
|
+
] = False,
|
|
162
|
+
dry_run: DryRunFlag = False,
|
|
163
|
+
json_: JsonFlag = False,
|
|
164
|
+
):
|
|
165
|
+
"""Create a page in a OneNote section."""
|
|
166
|
+
text = read_body(body, body_file)
|
|
167
|
+
section_id = onenote.resolve_section(client, section)
|
|
168
|
+
plan = onenote.plan_create_page(client, section_id, title=title, body=text, html=html_)
|
|
169
|
+
if dry_run:
|
|
170
|
+
return DryRunResult(plan)
|
|
171
|
+
obj = client.execute(plan[0])
|
|
172
|
+
return WriteResult(obj=obj, message=f"Created page {title!r}.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command("search")
|
|
176
|
+
@graph_command(scopes=["Notes.Read"])
|
|
177
|
+
def search(
|
|
178
|
+
client: GraphClient,
|
|
179
|
+
q: Annotated[str, typer.Argument(metavar="Q")],
|
|
180
|
+
limit: LimitOpt = None,
|
|
181
|
+
json_: JsonFlag = False,
|
|
182
|
+
):
|
|
183
|
+
"""Search OneNote page titles and content (Graph supports this for consumer notebooks only)."""
|
|
184
|
+
bound = limit if limit is not None else DEFAULT_LIMIT
|
|
185
|
+
page = onenote.search_pages(client, q, limit=bound)
|
|
186
|
+
tz = client.tz
|
|
187
|
+
return ListResult(
|
|
188
|
+
items=page.items,
|
|
189
|
+
page=page,
|
|
190
|
+
supports_all=False,
|
|
191
|
+
columns=[
|
|
192
|
+
Column("id", "id"),
|
|
193
|
+
Column("created", lambda pg: fmt_dt(pg.get("createdDateTime"), tz)),
|
|
194
|
+
Column("title", lambda pg: truncate(pg.get("title"))),
|
|
195
|
+
],
|
|
196
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Manager, direct reports and the management chain (spec §8.6)."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from mgraphctl.cli import (
|
|
8
|
+
AllFlag,
|
|
9
|
+
JsonFlag,
|
|
10
|
+
LimitOpt,
|
|
11
|
+
gate,
|
|
12
|
+
graph_command,
|
|
13
|
+
make_noun_app,
|
|
14
|
+
page_bounds,
|
|
15
|
+
)
|
|
16
|
+
from mgraphctl.errors import GraphError
|
|
17
|
+
from mgraphctl.graph import org
|
|
18
|
+
from mgraphctl.http import GraphClient
|
|
19
|
+
from mgraphctl.render import Column, ListResult, ObjectResult, TextResult
|
|
20
|
+
|
|
21
|
+
app = make_noun_app("Manager, direct reports, management chain.")
|
|
22
|
+
|
|
23
|
+
MANAGER_FIELDS = [
|
|
24
|
+
("Name", "displayName"),
|
|
25
|
+
("UPN", "userPrincipalName"),
|
|
26
|
+
("Mail", "mail"),
|
|
27
|
+
("Title", "jobTitle"),
|
|
28
|
+
("Dept", "department"),
|
|
29
|
+
("User id", "id"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _chain_line(level: int, person: dict) -> str:
|
|
34
|
+
upn = person.get("userPrincipalName") or ""
|
|
35
|
+
title = person.get("jobTitle")
|
|
36
|
+
suffix = f" — {title}" if title else ""
|
|
37
|
+
return f"{level}: {person.get('displayName')} <{upn}>{suffix}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command("manager")
|
|
41
|
+
@graph_command(scopes=["User.Read"])
|
|
42
|
+
def manager(
|
|
43
|
+
client: GraphClient,
|
|
44
|
+
upn: Annotated[str | None, typer.Argument(metavar="UPN")] = None,
|
|
45
|
+
json_: JsonFlag = False,
|
|
46
|
+
):
|
|
47
|
+
"""Show a user's manager (self by default)."""
|
|
48
|
+
if upn is not None:
|
|
49
|
+
gate(["User.Read.All"])
|
|
50
|
+
return ObjectResult(obj=org.manager(client, upn), fields=list(MANAGER_FIELDS))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("reports")
|
|
54
|
+
@graph_command(scopes=["User.Read"])
|
|
55
|
+
def reports(
|
|
56
|
+
client: GraphClient,
|
|
57
|
+
upn: Annotated[str | None, typer.Argument(metavar="UPN")] = None,
|
|
58
|
+
limit: LimitOpt = None,
|
|
59
|
+
all_: AllFlag = False,
|
|
60
|
+
json_: JsonFlag = False,
|
|
61
|
+
):
|
|
62
|
+
"""List a user's direct reports (self by default)."""
|
|
63
|
+
if upn is not None:
|
|
64
|
+
gate(["User.Read.All"])
|
|
65
|
+
limit, all_ = page_bounds(limit, all_, default=20)
|
|
66
|
+
page = org.reports(client, upn, limit=limit, all_=all_)
|
|
67
|
+
return ListResult(
|
|
68
|
+
items=page.items,
|
|
69
|
+
page=page,
|
|
70
|
+
hit_cap=org.CAP if all_ else None,
|
|
71
|
+
columns=[
|
|
72
|
+
Column("id", "id"),
|
|
73
|
+
Column("name", "displayName"),
|
|
74
|
+
Column("upn", "userPrincipalName"),
|
|
75
|
+
Column("mail", "mail"),
|
|
76
|
+
Column("title", "jobTitle"),
|
|
77
|
+
Column("department", "department"),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command("chain")
|
|
83
|
+
@graph_command(scopes=["User.Read"])
|
|
84
|
+
def chain(
|
|
85
|
+
client: GraphClient,
|
|
86
|
+
upn: Annotated[str | None, typer.Argument(metavar="UPN")] = None,
|
|
87
|
+
max_: Annotated[int, typer.Option("--max", min=1, help="Maximum levels to climb.")] = 10,
|
|
88
|
+
json_: JsonFlag = False,
|
|
89
|
+
):
|
|
90
|
+
"""Show the management chain from the user upward (self by default)."""
|
|
91
|
+
try:
|
|
92
|
+
levels = org.chain_expand(client, upn, max_levels=max_)
|
|
93
|
+
except GraphError as exc:
|
|
94
|
+
if exc.status not in (400, 403):
|
|
95
|
+
raise
|
|
96
|
+
gate(["User.Read.All"])
|
|
97
|
+
levels = org.chain_iterative(client, upn, max_levels=max_)
|
|
98
|
+
text = "\n".join(_chain_line(i, person) for i, person in enumerate(levels))
|
|
99
|
+
return TextResult(
|
|
100
|
+
text=text,
|
|
101
|
+
json_obj={"items": levels, "count": len(levels), "truncated": False},
|
|
102
|
+
)
|