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,304 @@
|
|
|
1
|
+
"""SharePoint sites, drives, browsing, lists and items (spec §8.12)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Literal
|
|
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
|
+
)
|
|
17
|
+
from mgraphctl.graph import files, sharepoint
|
|
18
|
+
from mgraphctl.http import GraphClient
|
|
19
|
+
from mgraphctl.render import (
|
|
20
|
+
Column,
|
|
21
|
+
DryRunResult,
|
|
22
|
+
FileResult,
|
|
23
|
+
ListResult,
|
|
24
|
+
ObjectResult,
|
|
25
|
+
WriteResult,
|
|
26
|
+
fmt_size,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
app = make_noun_app("SharePoint sites, drives and lists.")
|
|
30
|
+
|
|
31
|
+
SITE_FIELDS = [
|
|
32
|
+
("Name", "displayName"),
|
|
33
|
+
("Site name", "name"),
|
|
34
|
+
("Web URL", "webUrl"),
|
|
35
|
+
("Description", "description"),
|
|
36
|
+
("Id", "id"),
|
|
37
|
+
]
|
|
38
|
+
# Shared by `sites` and `lists`: both show id, a display name, and a webUrl.
|
|
39
|
+
SITE_LIST_COLUMNS = [Column("id", "id"), Column("name", "displayName"), Column("webUrl", "webUrl")]
|
|
40
|
+
LIST_COLUMNS = SITE_LIST_COLUMNS
|
|
41
|
+
DRIVE_COLUMNS = [
|
|
42
|
+
Column("id", "id"),
|
|
43
|
+
Column("name", "name"),
|
|
44
|
+
Column("type", "driveType"),
|
|
45
|
+
Column("webUrl", "webUrl"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
DriveOpt = Annotated[str | None, typer.Option("--drive", help="Drive name or id.")]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _open_drive(client: GraphClient, site: str, drive: str | None) -> tuple[str, str]:
|
|
52
|
+
"""Resolve `SITE` (and `--drive`, if given) to `(site_id, base)`.
|
|
53
|
+
|
|
54
|
+
Uses `resolve_site_id`, not `resolve_site`, so an already-known site id (composite
|
|
55
|
+
`host,guid,guid`, or `id:`-forced) needs no Graph call — required for `upload --dry-run`
|
|
56
|
+
to make zero requests.
|
|
57
|
+
"""
|
|
58
|
+
site_id = sharepoint.resolve_site_id(client, site)
|
|
59
|
+
drive_id = sharepoint.resolve_drive(client, site_id, drive) if drive else None
|
|
60
|
+
return site_id, sharepoint.drive_base(site_id, drive_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _item_columns(items: list[dict], fields: list[str] | None) -> list[Column]:
|
|
64
|
+
"""One column per requested `--fields` entry, else per field seen on the items (first 8)."""
|
|
65
|
+
if fields:
|
|
66
|
+
keys = fields[:8]
|
|
67
|
+
else:
|
|
68
|
+
keys = []
|
|
69
|
+
for item in items:
|
|
70
|
+
for key in item.get("fields") or {}:
|
|
71
|
+
if key not in keys:
|
|
72
|
+
keys.append(key)
|
|
73
|
+
if len(keys) >= 8:
|
|
74
|
+
break
|
|
75
|
+
keys = keys[:8]
|
|
76
|
+
return [
|
|
77
|
+
Column(key, (lambda k: lambda it: (it.get("fields") or {}).get(k))(key)) for key in keys
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command("sites")
|
|
82
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
83
|
+
def sites(
|
|
84
|
+
client: GraphClient,
|
|
85
|
+
search: Annotated[
|
|
86
|
+
str | None, typer.Option("--search", help="Search text (default '*').")
|
|
87
|
+
] = None,
|
|
88
|
+
limit: LimitOpt = None,
|
|
89
|
+
json_: JsonFlag = False,
|
|
90
|
+
):
|
|
91
|
+
"""List followed sites, or search all sites."""
|
|
92
|
+
page = sharepoint.list_sites(client, search=search, limit=limit if limit is not None else 20)
|
|
93
|
+
return ListResult(items=page.items, page=page, supports_all=False, columns=SITE_LIST_COLUMNS)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("site")
|
|
97
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
98
|
+
def site(
|
|
99
|
+
client: GraphClient, ref: Annotated[str, typer.Argument(metavar="REF")], json_: JsonFlag = False
|
|
100
|
+
):
|
|
101
|
+
"""Show one site, resolved by URL, `host:/a/b`, composite id, or name."""
|
|
102
|
+
return ObjectResult(obj=sharepoint.resolve_site(client, ref), fields=list(SITE_FIELDS))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command("drives")
|
|
106
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
107
|
+
def drives(
|
|
108
|
+
client: GraphClient,
|
|
109
|
+
site: Annotated[str, typer.Argument(metavar="SITE")],
|
|
110
|
+
json_: JsonFlag = False,
|
|
111
|
+
):
|
|
112
|
+
"""List a site's document libraries (drives)."""
|
|
113
|
+
site_obj = sharepoint.resolve_site(client, site)
|
|
114
|
+
items = sharepoint.list_drives(client, site_obj["id"])
|
|
115
|
+
return ListResult(items=items, columns=DRIVE_COLUMNS)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("ls")
|
|
119
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
120
|
+
def ls(
|
|
121
|
+
client: GraphClient,
|
|
122
|
+
site: Annotated[str, typer.Argument(metavar="SITE")],
|
|
123
|
+
path: Annotated[str | None, typer.Argument(metavar="PATH")] = None,
|
|
124
|
+
drive: DriveOpt = None,
|
|
125
|
+
limit: LimitOpt = None,
|
|
126
|
+
all_: AllFlag = False,
|
|
127
|
+
json_: JsonFlag = False,
|
|
128
|
+
):
|
|
129
|
+
"""List the children of a folder in the site drive (default: root)."""
|
|
130
|
+
limit_, all_val = page_bounds(limit, all_, default=50)
|
|
131
|
+
_site_id, base = _open_drive(client, site, drive)
|
|
132
|
+
page = files.list_children(client, base, path, limit=limit_, all_=all_val)
|
|
133
|
+
return ListResult(
|
|
134
|
+
items=page.items,
|
|
135
|
+
page=page,
|
|
136
|
+
hit_cap=files.CAP_LS if all_val else None,
|
|
137
|
+
columns=files.item_columns(client.tz),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command("search")
|
|
142
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
143
|
+
def search(
|
|
144
|
+
client: GraphClient,
|
|
145
|
+
q: Annotated[str, typer.Argument(metavar="Q")],
|
|
146
|
+
site: Annotated[
|
|
147
|
+
str | None, typer.Option("--site", help="Search within one site's drive.")
|
|
148
|
+
] = None,
|
|
149
|
+
limit: LimitOpt = None,
|
|
150
|
+
all_: AllFlag = False,
|
|
151
|
+
json_: JsonFlag = False,
|
|
152
|
+
):
|
|
153
|
+
"""Search a site's drive, or every site (Search API) when `--site` is not given."""
|
|
154
|
+
limit_, all_val = page_bounds(limit, all_, default=25)
|
|
155
|
+
if site:
|
|
156
|
+
site_obj = sharepoint.resolve_site(client, site)
|
|
157
|
+
page = sharepoint.search_site_drive(client, site_obj["id"], q, limit=limit_, all_=all_val)
|
|
158
|
+
items, truncated = page.items, page.truncated
|
|
159
|
+
else:
|
|
160
|
+
result = sharepoint.search_all(client, q, limit=limit_, all_=all_val)
|
|
161
|
+
items = [hit.get("resource") or {} for hit in result.hits]
|
|
162
|
+
truncated = result.more
|
|
163
|
+
return ListResult(
|
|
164
|
+
items=items,
|
|
165
|
+
truncated=truncated,
|
|
166
|
+
hit_cap=sharepoint.SEARCH_CAP if all_val else None,
|
|
167
|
+
columns=files.item_columns(client.tz),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command("download")
|
|
172
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
173
|
+
def download(
|
|
174
|
+
client: GraphClient,
|
|
175
|
+
site: Annotated[str, typer.Argument(metavar="SITE")],
|
|
176
|
+
item: Annotated[str, typer.Argument(metavar="ITEM_OR_PATH")],
|
|
177
|
+
output: Annotated[Path | None, typer.Option("--output", help="Destination file.")] = None,
|
|
178
|
+
drive: DriveOpt = None,
|
|
179
|
+
json_: JsonFlag = False,
|
|
180
|
+
):
|
|
181
|
+
"""Download a file from the site drive (fixes the Node quirk of using the wrong drive)."""
|
|
182
|
+
_site_id, base = _open_drive(client, site, drive)
|
|
183
|
+
graph_item, result = files.download_item(client, base, item, output)
|
|
184
|
+
return FileResult(
|
|
185
|
+
path=result.path,
|
|
186
|
+
bytes=result.bytes,
|
|
187
|
+
meta=dict(contentType=result.content_type, id=graph_item.get("id")),
|
|
188
|
+
message=f"Downloaded {graph_item.get('name')} ({fmt_size(result.bytes)}) to {result.path}",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.command("upload")
|
|
193
|
+
@graph_command(scopes=["Sites.ReadWrite.All"])
|
|
194
|
+
def upload(
|
|
195
|
+
client: GraphClient,
|
|
196
|
+
site: Annotated[str, typer.Argument(metavar="SITE")],
|
|
197
|
+
file: Annotated[Path, typer.Argument(metavar="FILE", exists=True, dir_okay=False)],
|
|
198
|
+
dest: Annotated[
|
|
199
|
+
str | None, typer.Option("--dest", help="Destination path (default: /basename).")
|
|
200
|
+
] = None,
|
|
201
|
+
drive: DriveOpt = None,
|
|
202
|
+
conflict: Annotated[
|
|
203
|
+
Literal["rename", "replace", "fail"], typer.Option("--conflict", help="On a name clash.")
|
|
204
|
+
] = "replace",
|
|
205
|
+
dry_run: DryRunFlag = False,
|
|
206
|
+
json_: JsonFlag = False,
|
|
207
|
+
):
|
|
208
|
+
"""Upload a file to the site drive."""
|
|
209
|
+
_site_id, base = _open_drive(client, site, drive)
|
|
210
|
+
plan = files.plan_upload(client, base, file, dest, conflict)
|
|
211
|
+
if dry_run:
|
|
212
|
+
return DryRunResult(plan)
|
|
213
|
+
obj = files.run_upload(client, base, file, dest, conflict)
|
|
214
|
+
return WriteResult(obj=obj, message=f"Uploaded {file.name} to {obj.get('webUrl') or base}.")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@app.command("url")
|
|
218
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
219
|
+
def url(
|
|
220
|
+
client: GraphClient,
|
|
221
|
+
url: Annotated[str, typer.Argument(metavar="URL")],
|
|
222
|
+
output: Annotated[Path | None, typer.Option("--output", help="Destination file.")] = None,
|
|
223
|
+
info: Annotated[
|
|
224
|
+
bool, typer.Option("--info", help="Resolve and print, without downloading.")
|
|
225
|
+
] = False,
|
|
226
|
+
json_: JsonFlag = False,
|
|
227
|
+
):
|
|
228
|
+
"""Resolve a SharePoint web URL to a drive item, and download it."""
|
|
229
|
+
if info:
|
|
230
|
+
resolution = sharepoint.resolve_url(client, url)
|
|
231
|
+
obj = {
|
|
232
|
+
"resolution": {
|
|
233
|
+
"siteId": resolution.site_id,
|
|
234
|
+
"driveId": resolution.drive_id,
|
|
235
|
+
"path": resolution.path,
|
|
236
|
+
},
|
|
237
|
+
"item": resolution.item,
|
|
238
|
+
}
|
|
239
|
+
fields = [
|
|
240
|
+
("siteId", "resolution.siteId"),
|
|
241
|
+
("driveId", "resolution.driveId"),
|
|
242
|
+
("path", "resolution.path"),
|
|
243
|
+
("item", "item.id"),
|
|
244
|
+
]
|
|
245
|
+
return ObjectResult(obj=obj, fields=fields)
|
|
246
|
+
_resolution, result = sharepoint.download_url(client, url, output)
|
|
247
|
+
return FileResult(
|
|
248
|
+
path=result.path,
|
|
249
|
+
bytes=result.bytes,
|
|
250
|
+
meta=dict(contentType=result.content_type),
|
|
251
|
+
message=f"Downloaded {result.path.name} ({fmt_size(result.bytes)}) to {result.path}",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@app.command("lists")
|
|
256
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
257
|
+
def lists_(
|
|
258
|
+
client: GraphClient,
|
|
259
|
+
site: Annotated[str, typer.Argument(metavar="SITE")],
|
|
260
|
+
json_: JsonFlag = False,
|
|
261
|
+
):
|
|
262
|
+
"""List a site's lists and document libraries (system lists hidden)."""
|
|
263
|
+
site_obj = sharepoint.resolve_site(client, site)
|
|
264
|
+
items = sharepoint.list_lists(client, site_obj["id"])
|
|
265
|
+
return ListResult(items=items, columns=LIST_COLUMNS)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@app.command("items")
|
|
269
|
+
@graph_command(scopes=["Sites.Read.All"])
|
|
270
|
+
def items_(
|
|
271
|
+
client: GraphClient,
|
|
272
|
+
site: Annotated[str, typer.Argument(metavar="SITE")],
|
|
273
|
+
list_: Annotated[str, typer.Argument(metavar="LIST")],
|
|
274
|
+
fields: Annotated[
|
|
275
|
+
str | None, typer.Option("--fields", help="Comma-separated field names.")
|
|
276
|
+
] = None,
|
|
277
|
+
filter_: Annotated[
|
|
278
|
+
str | None, typer.Option("--filter", help="OData filter on fields/*.")
|
|
279
|
+
] = None,
|
|
280
|
+
limit: LimitOpt = None,
|
|
281
|
+
all_: AllFlag = False,
|
|
282
|
+
json_: JsonFlag = False,
|
|
283
|
+
):
|
|
284
|
+
"""List a SharePoint list's items, with their column values."""
|
|
285
|
+
limit_, all_val = page_bounds(limit, all_, default=50)
|
|
286
|
+
site_obj = sharepoint.resolve_site(client, site)
|
|
287
|
+
site_id = site_obj["id"]
|
|
288
|
+
list_obj = sharepoint.resolve_list(client, site_id, list_)
|
|
289
|
+
field_list = [f.strip() for f in fields.split(",")] if fields else None
|
|
290
|
+
page = sharepoint.list_items(
|
|
291
|
+
client,
|
|
292
|
+
site_id,
|
|
293
|
+
list_obj["id"],
|
|
294
|
+
fields=field_list,
|
|
295
|
+
filter_=filter_,
|
|
296
|
+
limit=limit_,
|
|
297
|
+
all_=all_val,
|
|
298
|
+
)
|
|
299
|
+
return ListResult(
|
|
300
|
+
items=page.items,
|
|
301
|
+
page=page,
|
|
302
|
+
hit_cap=sharepoint.ITEMS_CAP if all_val else None,
|
|
303
|
+
columns=_item_columns(page.items, field_list),
|
|
304
|
+
)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Teams and channel commands (spec §8.7)."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from mgraphctl.cli import (
|
|
8
|
+
AllFlag,
|
|
9
|
+
DryRunFlag,
|
|
10
|
+
JsonFlag,
|
|
11
|
+
LimitOpt,
|
|
12
|
+
graph_command,
|
|
13
|
+
make_noun_app,
|
|
14
|
+
page_bounds,
|
|
15
|
+
read_body,
|
|
16
|
+
)
|
|
17
|
+
from mgraphctl.errors import UsageError
|
|
18
|
+
from mgraphctl.graph import teams
|
|
19
|
+
from mgraphctl.http import GraphClient
|
|
20
|
+
from mgraphctl.render import (
|
|
21
|
+
Column,
|
|
22
|
+
DryRunResult,
|
|
23
|
+
ListResult,
|
|
24
|
+
ObjectResult,
|
|
25
|
+
Window,
|
|
26
|
+
WriteResult,
|
|
27
|
+
parse_dt,
|
|
28
|
+
truncate,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
READ_TEAM = ["Team.ReadBasic.All|Group.Read.All"]
|
|
32
|
+
READ_CHANNEL = ["Channel.ReadBasic.All|Group.Read.All"]
|
|
33
|
+
|
|
34
|
+
TeamArg = Annotated[str, typer.Argument(metavar="TEAM", help="Team GUID or display name.")]
|
|
35
|
+
ChannelArg = Annotated[
|
|
36
|
+
str, typer.Argument(metavar="CHANNEL", help="Channel id (19:…) or display name.")
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
TEAM_FIELDS = [
|
|
40
|
+
("Name", "displayName"),
|
|
41
|
+
("Description", "description"),
|
|
42
|
+
("Visibility", "visibility"),
|
|
43
|
+
("Web URL", "webUrl"),
|
|
44
|
+
("Team id", "id"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
CHANNEL_FIELDS = [
|
|
48
|
+
("Name", "displayName"),
|
|
49
|
+
("Description", "description"),
|
|
50
|
+
("Membership", "membershipType"),
|
|
51
|
+
("Web URL", "webUrl"),
|
|
52
|
+
("Channel id", "id"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
app = make_noun_app("Teams and channels.")
|
|
56
|
+
channel = make_noun_app("Channels within a team.")
|
|
57
|
+
app.add_typer(channel, name="channel")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command("list")
|
|
61
|
+
@graph_command(scopes=READ_TEAM)
|
|
62
|
+
def list_(client: GraphClient, json_: JsonFlag = False):
|
|
63
|
+
"""List the teams you belong to."""
|
|
64
|
+
page = teams.list_teams(client)
|
|
65
|
+
return ListResult(
|
|
66
|
+
items=page.items,
|
|
67
|
+
page=page,
|
|
68
|
+
supports_all=False,
|
|
69
|
+
empty_text="No teams.",
|
|
70
|
+
columns=[
|
|
71
|
+
Column("id", "id"),
|
|
72
|
+
Column("name", "displayName"),
|
|
73
|
+
Column("description", lambda t: truncate(t.get("description"))),
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("get")
|
|
79
|
+
@graph_command(scopes=READ_TEAM)
|
|
80
|
+
def get(client: GraphClient, team: TeamArg, json_: JsonFlag = False):
|
|
81
|
+
"""Show one team."""
|
|
82
|
+
team_id = teams.resolve_team(client, team)["id"]
|
|
83
|
+
return ObjectResult(obj=teams.get_team(client, team_id), fields=list(TEAM_FIELDS))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("members")
|
|
87
|
+
@graph_command(scopes=["TeamMember.Read.All"])
|
|
88
|
+
def members(
|
|
89
|
+
client: GraphClient,
|
|
90
|
+
team: TeamArg,
|
|
91
|
+
limit: LimitOpt = None,
|
|
92
|
+
all_: AllFlag = False,
|
|
93
|
+
json_: JsonFlag = False,
|
|
94
|
+
):
|
|
95
|
+
"""List a team's members."""
|
|
96
|
+
limit, all_ = page_bounds(limit, all_, default=100)
|
|
97
|
+
team_id = teams.resolve_team(client, team)["id"]
|
|
98
|
+
page = teams.list_members(client, team_id, limit=limit, all_=all_)
|
|
99
|
+
return ListResult(
|
|
100
|
+
items=page.items,
|
|
101
|
+
page=page,
|
|
102
|
+
hit_cap=teams.CAP_MEMBERS if all_ else None,
|
|
103
|
+
empty_text="No members.",
|
|
104
|
+
columns=[
|
|
105
|
+
Column("id", "id"),
|
|
106
|
+
Column("name", "displayName"),
|
|
107
|
+
Column("email", "email"),
|
|
108
|
+
Column("roles", "roles"),
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command("channels")
|
|
114
|
+
@graph_command(scopes=READ_CHANNEL)
|
|
115
|
+
def channels(client: GraphClient, team: TeamArg, json_: JsonFlag = False):
|
|
116
|
+
"""List a team's channels."""
|
|
117
|
+
team_id = teams.resolve_team(client, team)["id"]
|
|
118
|
+
page = teams.list_channels(client, team_id)
|
|
119
|
+
return ListResult(
|
|
120
|
+
items=page.items,
|
|
121
|
+
page=page,
|
|
122
|
+
supports_all=False,
|
|
123
|
+
empty_text="No channels.",
|
|
124
|
+
columns=[
|
|
125
|
+
Column("id", "id"),
|
|
126
|
+
Column("name", "displayName"),
|
|
127
|
+
Column("membership", "membershipType"),
|
|
128
|
+
Column("description", lambda c: truncate(c.get("description"))),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@channel.command("get")
|
|
134
|
+
@graph_command(scopes=READ_CHANNEL)
|
|
135
|
+
def channel_get(client: GraphClient, team: TeamArg, chan: ChannelArg, json_: JsonFlag = False):
|
|
136
|
+
"""Show one channel."""
|
|
137
|
+
team_id = teams.resolve_team(client, team)["id"]
|
|
138
|
+
channel_id = teams.resolve_channel(client, team_id, chan)["id"]
|
|
139
|
+
return ObjectResult(
|
|
140
|
+
obj=teams.get_channel(client, team_id, channel_id), fields=list(CHANNEL_FIELDS)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@channel.command("messages")
|
|
145
|
+
@graph_command(scopes=["ChannelMessage.Read.All"])
|
|
146
|
+
def channel_messages(
|
|
147
|
+
client: GraphClient,
|
|
148
|
+
team: TeamArg,
|
|
149
|
+
chan: ChannelArg,
|
|
150
|
+
full: Annotated[
|
|
151
|
+
bool, typer.Option("--full", help="Print whole bodies, not 300 chars.")
|
|
152
|
+
] = False,
|
|
153
|
+
with_replies: Annotated[
|
|
154
|
+
bool, typer.Option("--with-replies", help="Expand each message's replies.")
|
|
155
|
+
] = False,
|
|
156
|
+
replies: Annotated[
|
|
157
|
+
str | None,
|
|
158
|
+
typer.Option("--replies", metavar="MSGID", help="List the replies to one message."),
|
|
159
|
+
] = None,
|
|
160
|
+
after: Annotated[
|
|
161
|
+
str | None, typer.Option("--after", metavar="DT", help="Only messages touched after this.")
|
|
162
|
+
] = None,
|
|
163
|
+
before: Annotated[
|
|
164
|
+
str | None,
|
|
165
|
+
typer.Option("--before", metavar="DT", help="Only messages touched before this."),
|
|
166
|
+
] = None,
|
|
167
|
+
limit: LimitOpt = None,
|
|
168
|
+
all_: AllFlag = False,
|
|
169
|
+
json_: JsonFlag = False,
|
|
170
|
+
):
|
|
171
|
+
"""List channel messages, oldest first in text mode."""
|
|
172
|
+
if replies is not None and with_replies:
|
|
173
|
+
raise UsageError("USAGE", "--replies and --with-replies are mutually exclusive")
|
|
174
|
+
if replies is not None and (after is not None or before is not None):
|
|
175
|
+
raise UsageError("USAGE", "--after/--before do not apply to --replies")
|
|
176
|
+
tz = client.tz
|
|
177
|
+
after_dt = parse_dt(after, tz) if after else None
|
|
178
|
+
before_dt = parse_dt(before, tz, end_of_day=True) if before else None
|
|
179
|
+
if after_dt is not None and before_dt is not None and after_dt > before_dt:
|
|
180
|
+
raise UsageError("USAGE", "--after must not be later than --before")
|
|
181
|
+
limit, all_ = page_bounds(limit, all_, default=20)
|
|
182
|
+
team_id = teams.resolve_team(client, team)["id"]
|
|
183
|
+
channel_id = teams.resolve_channel(client, team_id, chan)["id"]
|
|
184
|
+
if replies is not None:
|
|
185
|
+
page = teams.channel_replies(client, team_id, channel_id, replies, limit=limit, all_=all_)
|
|
186
|
+
else:
|
|
187
|
+
page = teams.channel_messages(
|
|
188
|
+
client,
|
|
189
|
+
team_id,
|
|
190
|
+
channel_id,
|
|
191
|
+
with_replies=with_replies,
|
|
192
|
+
after=after_dt,
|
|
193
|
+
before=before_dt,
|
|
194
|
+
limit=limit,
|
|
195
|
+
all_=all_,
|
|
196
|
+
)
|
|
197
|
+
# JSON keeps the Graph order (newest first); text reads better oldest first (§10 quirk 17).
|
|
198
|
+
return ListResult(
|
|
199
|
+
items=page.items if json_ else list(reversed(page.items)),
|
|
200
|
+
page=page,
|
|
201
|
+
window=Window(after=after_dt, before=before_dt),
|
|
202
|
+
hit_cap=teams.CAP_MESSAGES if all_ else None,
|
|
203
|
+
empty_text="No messages.",
|
|
204
|
+
columns=teams.message_columns(client.tz, full),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@channel.command("send")
|
|
209
|
+
@graph_command(scopes=["ChannelMessage.Send"])
|
|
210
|
+
def channel_send(
|
|
211
|
+
client: GraphClient,
|
|
212
|
+
team: TeamArg,
|
|
213
|
+
chan: ChannelArg,
|
|
214
|
+
body: Annotated[str | None, typer.Option("--body", help="Message text.")] = None,
|
|
215
|
+
body_file: Annotated[
|
|
216
|
+
str | None, typer.Option("--body-file", help="File holding the text, or - for stdin.")
|
|
217
|
+
] = None,
|
|
218
|
+
html: Annotated[bool, typer.Option("--html", help="Send the body as HTML.")] = False,
|
|
219
|
+
subject: Annotated[str | None, typer.Option("--subject", help="Message subject.")] = None,
|
|
220
|
+
reply_to: Annotated[
|
|
221
|
+
str | None, typer.Option("--reply-to", metavar="MSGID", help="Reply in this thread.")
|
|
222
|
+
] = None,
|
|
223
|
+
dry_run: DryRunFlag = False,
|
|
224
|
+
json_: JsonFlag = False,
|
|
225
|
+
):
|
|
226
|
+
"""Post a message to a channel."""
|
|
227
|
+
text = read_body(body, body_file)
|
|
228
|
+
team_id = teams.resolve_team(client, team)["id"]
|
|
229
|
+
channel_id = teams.resolve_channel(client, team_id, chan)["id"]
|
|
230
|
+
plan = teams.plan_channel_send(
|
|
231
|
+
client, team_id, channel_id, body=text, html=html, subject=subject, reply_to=reply_to
|
|
232
|
+
)
|
|
233
|
+
if dry_run:
|
|
234
|
+
return DryRunResult(plan)
|
|
235
|
+
return WriteResult(obj=client.execute(plan[0]), message="Sent.")
|