meshagent-cli 0.21.0__py3-none-any.whl → 0.23.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.
- meshagent/cli/agent.py +8 -2
- meshagent/cli/call.py +15 -28
- meshagent/cli/chatbot.py +580 -76
- meshagent/cli/cli.py +3 -3
- meshagent/cli/helper.py +40 -2
- meshagent/cli/helpers.py +0 -3
- meshagent/cli/host.py +4 -0
- meshagent/cli/mailbot.py +137 -76
- meshagent/cli/meeting_transcriber.py +19 -11
- meshagent/cli/messaging.py +1 -4
- meshagent/cli/multi.py +53 -98
- meshagent/cli/oauth2.py +164 -35
- meshagent/cli/room.py +6 -2
- meshagent/cli/services.py +238 -15
- meshagent/cli/sync.py +434 -0
- meshagent/cli/task_runner.py +625 -78
- meshagent/cli/version.py +1 -1
- meshagent/cli/voicebot.py +54 -34
- meshagent/cli/worker.py +151 -75
- {meshagent_cli-0.21.0.dist-info → meshagent_cli-0.23.0.dist-info}/METADATA +13 -11
- meshagent_cli-0.23.0.dist-info/RECORD +45 -0
- {meshagent_cli-0.21.0.dist-info → meshagent_cli-0.23.0.dist-info}/WHEEL +1 -1
- meshagent_cli-0.21.0.dist-info/RECORD +0 -44
- {meshagent_cli-0.21.0.dist-info → meshagent_cli-0.23.0.dist-info}/entry_points.txt +0 -0
- {meshagent_cli-0.21.0.dist-info → meshagent_cli-0.23.0.dist-info}/top_level.txt +0 -0
meshagent/cli/sync.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich import print
|
|
10
|
+
|
|
11
|
+
from meshagent.api import RoomClient, RoomException, WebSocketClientProtocol
|
|
12
|
+
from meshagent.api.helpers import meshagent_base_url, websocket_room_url
|
|
13
|
+
from meshagent.api.runtime import RuntimeDocument
|
|
14
|
+
from meshagent.api.schema import MeshSchema
|
|
15
|
+
from meshagent.api.schema_document import Element
|
|
16
|
+
from meshagent.cli import async_typer
|
|
17
|
+
from meshagent.cli.common_options import ProjectIdOption, RoomOption
|
|
18
|
+
from meshagent.cli.helper import get_client, resolve_project_id, resolve_room
|
|
19
|
+
|
|
20
|
+
app = async_typer.AsyncTyper(help="Inspect and update mesh documents in a room")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_json_arg(json_str: Optional[str], *, name: str) -> Any:
|
|
24
|
+
if json_str is None:
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(json_str)
|
|
28
|
+
except Exception as exc:
|
|
29
|
+
raise typer.BadParameter(f"Invalid JSON for {name}: {exc}") from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_json_file(path: Optional[Path], *, name: str) -> Any:
|
|
33
|
+
if path is None:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(path.read_text())
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
raise typer.BadParameter(f"Unable to read {name} from {path}: {exc}") from exc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _decode_pointer(path: str) -> list[str]:
|
|
42
|
+
if path == "":
|
|
43
|
+
return []
|
|
44
|
+
if not path.startswith("/"):
|
|
45
|
+
raise typer.BadParameter(f"Invalid JSON pointer: {path}")
|
|
46
|
+
tokens = path.lstrip("/").split("/")
|
|
47
|
+
return [token.replace("~1", "/").replace("~0", "~") for token in tokens]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_target(document: Any, tokens: list[str]) -> Any:
|
|
51
|
+
current = document
|
|
52
|
+
for token in tokens:
|
|
53
|
+
if isinstance(current, list):
|
|
54
|
+
if token == "-":
|
|
55
|
+
raise typer.BadParameter("JSON pointer '-' is not valid here")
|
|
56
|
+
try:
|
|
57
|
+
index = int(token)
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
raise typer.BadParameter(f"Invalid list index: {token}") from exc
|
|
60
|
+
try:
|
|
61
|
+
current = current[index]
|
|
62
|
+
except IndexError as exc:
|
|
63
|
+
raise typer.BadParameter(f"List index out of range: {token}") from exc
|
|
64
|
+
elif isinstance(current, dict):
|
|
65
|
+
if token not in current:
|
|
66
|
+
raise typer.BadParameter(f"Path not found: {token}")
|
|
67
|
+
current = current[token]
|
|
68
|
+
else:
|
|
69
|
+
raise typer.BadParameter("JSON pointer targets a non-container value")
|
|
70
|
+
return current
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _resolve_parent(document: Any, tokens: list[str]) -> tuple[Any, str]:
|
|
74
|
+
if not tokens:
|
|
75
|
+
raise typer.BadParameter("JSON pointer must target a child value")
|
|
76
|
+
parent = _resolve_target(document, tokens[:-1]) if len(tokens) > 1 else document
|
|
77
|
+
return parent, tokens[-1]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _add_value(document: Any, tokens: list[str], value: Any) -> Any:
|
|
81
|
+
if not tokens:
|
|
82
|
+
return value
|
|
83
|
+
parent, key = _resolve_parent(document, tokens)
|
|
84
|
+
if isinstance(parent, list):
|
|
85
|
+
if key == "-":
|
|
86
|
+
parent.append(value)
|
|
87
|
+
else:
|
|
88
|
+
try:
|
|
89
|
+
index = int(key)
|
|
90
|
+
except ValueError as exc:
|
|
91
|
+
raise typer.BadParameter(f"Invalid list index: {key}") from exc
|
|
92
|
+
if index < 0 or index > len(parent):
|
|
93
|
+
raise typer.BadParameter(f"List index out of range: {key}")
|
|
94
|
+
parent.insert(index, value)
|
|
95
|
+
elif isinstance(parent, dict):
|
|
96
|
+
parent[key] = value
|
|
97
|
+
else:
|
|
98
|
+
raise typer.BadParameter("JSON pointer targets a non-container value")
|
|
99
|
+
return document
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _replace_value(document: Any, tokens: list[str], value: Any) -> Any:
|
|
103
|
+
if not tokens:
|
|
104
|
+
return value
|
|
105
|
+
parent, key = _resolve_parent(document, tokens)
|
|
106
|
+
if isinstance(parent, list):
|
|
107
|
+
try:
|
|
108
|
+
index = int(key)
|
|
109
|
+
except ValueError as exc:
|
|
110
|
+
raise typer.BadParameter(f"Invalid list index: {key}") from exc
|
|
111
|
+
if index < 0 or index >= len(parent):
|
|
112
|
+
raise typer.BadParameter(f"List index out of range: {key}")
|
|
113
|
+
parent[index] = value
|
|
114
|
+
elif isinstance(parent, dict):
|
|
115
|
+
if key not in parent:
|
|
116
|
+
raise typer.BadParameter(f"Path not found: {key}")
|
|
117
|
+
parent[key] = value
|
|
118
|
+
else:
|
|
119
|
+
raise typer.BadParameter("JSON pointer targets a non-container value")
|
|
120
|
+
return document
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _remove_value(document: Any, tokens: list[str]) -> tuple[Any, Any]:
|
|
124
|
+
parent, key = _resolve_parent(document, tokens)
|
|
125
|
+
if isinstance(parent, list):
|
|
126
|
+
try:
|
|
127
|
+
index = int(key)
|
|
128
|
+
except ValueError as exc:
|
|
129
|
+
raise typer.BadParameter(f"Invalid list index: {key}") from exc
|
|
130
|
+
if index < 0 or index >= len(parent):
|
|
131
|
+
raise typer.BadParameter(f"List index out of range: {key}")
|
|
132
|
+
value = parent.pop(index)
|
|
133
|
+
elif isinstance(parent, dict):
|
|
134
|
+
if key not in parent:
|
|
135
|
+
raise typer.BadParameter(f"Path not found: {key}")
|
|
136
|
+
value = parent.pop(key)
|
|
137
|
+
else:
|
|
138
|
+
raise typer.BadParameter("JSON pointer targets a non-container value")
|
|
139
|
+
return document, value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _apply_json_patch(document: Any, patch_ops: list[dict[str, Any]]) -> Any:
|
|
143
|
+
updated = copy.deepcopy(document)
|
|
144
|
+
|
|
145
|
+
for op in patch_ops:
|
|
146
|
+
operation = op.get("op")
|
|
147
|
+
path = op.get("path")
|
|
148
|
+
if operation is None or path is None:
|
|
149
|
+
raise typer.BadParameter("Patch entries must include 'op' and 'path'")
|
|
150
|
+
|
|
151
|
+
tokens = _decode_pointer(path)
|
|
152
|
+
|
|
153
|
+
if operation == "add":
|
|
154
|
+
updated = _add_value(updated, tokens, op.get("value"))
|
|
155
|
+
elif operation == "replace":
|
|
156
|
+
updated = _replace_value(updated, tokens, op.get("value"))
|
|
157
|
+
elif operation == "remove":
|
|
158
|
+
if not tokens:
|
|
159
|
+
raise typer.BadParameter("Cannot remove the document root")
|
|
160
|
+
updated, _ = _remove_value(updated, tokens)
|
|
161
|
+
elif operation == "move":
|
|
162
|
+
from_path = op.get("from")
|
|
163
|
+
if from_path is None:
|
|
164
|
+
raise typer.BadParameter("Move operations require 'from'")
|
|
165
|
+
from_tokens = _decode_pointer(from_path)
|
|
166
|
+
updated, value = _remove_value(updated, from_tokens)
|
|
167
|
+
updated = _add_value(updated, tokens, value)
|
|
168
|
+
elif operation == "copy":
|
|
169
|
+
from_path = op.get("from")
|
|
170
|
+
if from_path is None:
|
|
171
|
+
raise typer.BadParameter("Copy operations require 'from'")
|
|
172
|
+
from_tokens = _decode_pointer(from_path)
|
|
173
|
+
value = copy.deepcopy(_resolve_target(updated, from_tokens))
|
|
174
|
+
updated = _add_value(updated, tokens, value)
|
|
175
|
+
elif operation == "test":
|
|
176
|
+
expected = op.get("value")
|
|
177
|
+
actual = _resolve_target(updated, tokens)
|
|
178
|
+
if actual != expected:
|
|
179
|
+
raise typer.BadParameter(f"Test operation failed at {path}")
|
|
180
|
+
else:
|
|
181
|
+
raise typer.BadParameter(f"Unsupported patch op: {operation}")
|
|
182
|
+
|
|
183
|
+
return updated
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _extract_element_payload(element_json: dict) -> tuple[str, dict]:
|
|
187
|
+
if len(element_json) != 1:
|
|
188
|
+
raise typer.BadParameter("Element JSON must have a single key")
|
|
189
|
+
tag_name = next(iter(element_json))
|
|
190
|
+
payload = element_json[tag_name] or {}
|
|
191
|
+
if not isinstance(payload, dict):
|
|
192
|
+
raise typer.BadParameter("Element payload must be an object")
|
|
193
|
+
return tag_name, payload
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _apply_element_json(element: Element, element_json: dict) -> None:
|
|
197
|
+
tag_name, payload = _extract_element_payload(element_json)
|
|
198
|
+
if tag_name != element.tag_name:
|
|
199
|
+
raise typer.BadParameter(
|
|
200
|
+
f"Patch root tag '{tag_name}' does not match document root '{element.tag_name}'"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
child_property = element.schema.child_property_name
|
|
204
|
+
children_json = []
|
|
205
|
+
if child_property is not None and child_property in payload:
|
|
206
|
+
children_json = payload.get(child_property) or []
|
|
207
|
+
if not isinstance(children_json, list):
|
|
208
|
+
raise typer.BadParameter("Child property must be a list")
|
|
209
|
+
|
|
210
|
+
attributes = {key: value for key, value in payload.items() if key != child_property}
|
|
211
|
+
|
|
212
|
+
for key in list(element._data["attributes"].keys()):
|
|
213
|
+
if key == "$id":
|
|
214
|
+
continue
|
|
215
|
+
if key not in attributes:
|
|
216
|
+
element._remove_attribute(key)
|
|
217
|
+
|
|
218
|
+
for key, value in attributes.items():
|
|
219
|
+
element.set_attribute(key, value)
|
|
220
|
+
|
|
221
|
+
if child_property is None:
|
|
222
|
+
if children_json:
|
|
223
|
+
raise typer.BadParameter("Element does not support children")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
for child in list(element.get_children()):
|
|
227
|
+
if isinstance(child, Element):
|
|
228
|
+
child.delete()
|
|
229
|
+
|
|
230
|
+
for child_json in children_json:
|
|
231
|
+
element.append_json(child_json)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _apply_document_json(doc: RuntimeDocument, updated_json: dict) -> None:
|
|
235
|
+
_apply_element_json(doc.root, updated_json)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _render_json(payload: Any, pretty: bool) -> None:
|
|
239
|
+
indent = 2 if pretty else None
|
|
240
|
+
print(json.dumps(payload, indent=indent))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def _connect_room(project_id: ProjectIdOption, room: RoomOption):
|
|
244
|
+
account_client = await get_client()
|
|
245
|
+
room_name = resolve_room(room)
|
|
246
|
+
if not room_name:
|
|
247
|
+
print("[red]Room name is required.[/red]")
|
|
248
|
+
raise typer.Exit(1)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
252
|
+
connection = await account_client.connect_room(
|
|
253
|
+
project_id=project_id, room=room_name
|
|
254
|
+
)
|
|
255
|
+
client = RoomClient(
|
|
256
|
+
protocol=WebSocketClientProtocol(
|
|
257
|
+
url=websocket_room_url(
|
|
258
|
+
room_name=room_name, base_url=meshagent_base_url()
|
|
259
|
+
),
|
|
260
|
+
token=connection.jwt,
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
await client.__aenter__()
|
|
264
|
+
return account_client, client
|
|
265
|
+
except Exception:
|
|
266
|
+
await account_client.close()
|
|
267
|
+
raise
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.async_command("show", help="Print the full document JSON")
|
|
271
|
+
async def sync_show(
|
|
272
|
+
*,
|
|
273
|
+
project_id: ProjectIdOption,
|
|
274
|
+
room: RoomOption,
|
|
275
|
+
path: str,
|
|
276
|
+
include_ids: bool = typer.Option(
|
|
277
|
+
False, "--include-ids", help="Include $id attributes in output"
|
|
278
|
+
),
|
|
279
|
+
pretty: bool = typer.Option(True, "--pretty/--compact", help="Pretty-print JSON"),
|
|
280
|
+
):
|
|
281
|
+
account_client, client = await _connect_room(project_id, room)
|
|
282
|
+
try:
|
|
283
|
+
doc = await client.sync.open(path=path, create=False)
|
|
284
|
+
try:
|
|
285
|
+
payload = doc.root.to_json(include_ids=include_ids)
|
|
286
|
+
_render_json(payload, pretty)
|
|
287
|
+
finally:
|
|
288
|
+
await client.sync.close(path=path)
|
|
289
|
+
except RoomException as exc:
|
|
290
|
+
print(f"[red]{exc}[/red]")
|
|
291
|
+
raise typer.Exit(1)
|
|
292
|
+
finally:
|
|
293
|
+
await client.__aexit__(None, None, None)
|
|
294
|
+
await account_client.close()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@app.async_command("grep", help="Search the document for matching content")
|
|
298
|
+
async def sync_grep(
|
|
299
|
+
*,
|
|
300
|
+
project_id: ProjectIdOption,
|
|
301
|
+
room: RoomOption,
|
|
302
|
+
path: str,
|
|
303
|
+
pattern: str = typer.Argument(..., help="Regex pattern to match"),
|
|
304
|
+
ignore_case: bool = typer.Option(False, "--ignore-case", help="Ignore case"),
|
|
305
|
+
before: int = typer.Option(0, "--before", min=0, help="Include siblings before"),
|
|
306
|
+
after: int = typer.Option(0, "--after", min=0, help="Include siblings after"),
|
|
307
|
+
include_ids: bool = typer.Option(
|
|
308
|
+
False, "--include-ids", help="Include $id attributes in output"
|
|
309
|
+
),
|
|
310
|
+
pretty: bool = typer.Option(True, "--pretty/--compact", help="Pretty-print JSON"),
|
|
311
|
+
):
|
|
312
|
+
account_client, client = await _connect_room(project_id, room)
|
|
313
|
+
try:
|
|
314
|
+
doc = await client.sync.open(path=path, create=False)
|
|
315
|
+
try:
|
|
316
|
+
matches = doc.root.grep(
|
|
317
|
+
pattern, ignore_case=ignore_case, before=before, after=after
|
|
318
|
+
)
|
|
319
|
+
payload = [match.to_json(include_ids=include_ids) for match in matches]
|
|
320
|
+
_render_json(payload, pretty)
|
|
321
|
+
finally:
|
|
322
|
+
await client.sync.close(path=path)
|
|
323
|
+
except RoomException as exc:
|
|
324
|
+
print(f"[red]{exc}[/red]")
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
finally:
|
|
327
|
+
await client.__aexit__(None, None, None)
|
|
328
|
+
await account_client.close()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.async_command("inspect", help="Print the document schema JSON")
|
|
332
|
+
async def sync_inspect(
|
|
333
|
+
*,
|
|
334
|
+
project_id: ProjectIdOption,
|
|
335
|
+
room: RoomOption,
|
|
336
|
+
path: str,
|
|
337
|
+
pretty: bool = typer.Option(True, "--pretty/--compact", help="Pretty-print JSON"),
|
|
338
|
+
):
|
|
339
|
+
account_client, client = await _connect_room(project_id, room)
|
|
340
|
+
try:
|
|
341
|
+
doc = await client.sync.open(path=path, create=False)
|
|
342
|
+
try:
|
|
343
|
+
_render_json(doc.schema.to_json(), pretty)
|
|
344
|
+
finally:
|
|
345
|
+
await client.sync.close(path=path)
|
|
346
|
+
except RoomException as exc:
|
|
347
|
+
print(f"[red]{exc}[/red]")
|
|
348
|
+
raise typer.Exit(1)
|
|
349
|
+
finally:
|
|
350
|
+
await client.__aexit__(None, None, None)
|
|
351
|
+
await account_client.close()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@app.async_command("create", help="Create a new document at a path")
|
|
355
|
+
async def sync_create(
|
|
356
|
+
*,
|
|
357
|
+
project_id: ProjectIdOption,
|
|
358
|
+
room: RoomOption,
|
|
359
|
+
path: str,
|
|
360
|
+
schema: Path = typer.Option(..., "--schema", help="Schema JSON file"),
|
|
361
|
+
json_payload: Optional[str] = typer.Option(
|
|
362
|
+
None, "--json", help="Initial JSON payload"
|
|
363
|
+
),
|
|
364
|
+
json_file: Optional[Path] = typer.Option(
|
|
365
|
+
None, "--json-file", help="Path to initial JSON payload"
|
|
366
|
+
),
|
|
367
|
+
):
|
|
368
|
+
initial_json = _load_json_file(json_file, name="json")
|
|
369
|
+
if initial_json is None:
|
|
370
|
+
initial_json = _parse_json_arg(json_payload, name="json")
|
|
371
|
+
|
|
372
|
+
schema_json = _load_json_file(schema, name="schema")
|
|
373
|
+
if schema_json is None:
|
|
374
|
+
raise typer.BadParameter("--schema is required")
|
|
375
|
+
|
|
376
|
+
account_client, client = await _connect_room(project_id, room)
|
|
377
|
+
try:
|
|
378
|
+
if await client.storage.exists(path=path):
|
|
379
|
+
print(f"[red]Document already exists at {path}.[/red]")
|
|
380
|
+
raise typer.Exit(1)
|
|
381
|
+
|
|
382
|
+
await client.sync.open(
|
|
383
|
+
path=path,
|
|
384
|
+
create=True,
|
|
385
|
+
initial_json=initial_json,
|
|
386
|
+
schema=MeshSchema.from_json(schema_json),
|
|
387
|
+
)
|
|
388
|
+
await client.sync.close(path=path)
|
|
389
|
+
print(f"[green]Created document at {path}[/green]")
|
|
390
|
+
except RoomException as exc:
|
|
391
|
+
print(f"[red]{exc}[/red]")
|
|
392
|
+
raise typer.Exit(1)
|
|
393
|
+
finally:
|
|
394
|
+
await client.__aexit__(None, None, None)
|
|
395
|
+
await account_client.close()
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@app.async_command("update", help="Apply a JSON patch to a document")
|
|
399
|
+
async def sync_update(
|
|
400
|
+
*,
|
|
401
|
+
project_id: ProjectIdOption,
|
|
402
|
+
room: RoomOption,
|
|
403
|
+
path: str,
|
|
404
|
+
patch: Optional[str] = typer.Option(None, "--patch", help="JSON patch array"),
|
|
405
|
+
patch_file: Optional[Path] = typer.Option(
|
|
406
|
+
None, "--patch-file", help="Path to JSON patch array"
|
|
407
|
+
),
|
|
408
|
+
):
|
|
409
|
+
patch_ops = _load_json_file(patch_file, name="patch")
|
|
410
|
+
if patch_ops is None:
|
|
411
|
+
patch_ops = _parse_json_arg(patch, name="patch")
|
|
412
|
+
if patch_ops is None:
|
|
413
|
+
raise typer.BadParameter("Provide --patch or --patch-file")
|
|
414
|
+
if not isinstance(patch_ops, list):
|
|
415
|
+
raise typer.BadParameter("Patch must be a JSON array")
|
|
416
|
+
|
|
417
|
+
account_client, client = await _connect_room(project_id, room)
|
|
418
|
+
try:
|
|
419
|
+
doc = await client.sync.open(path=path, create=False)
|
|
420
|
+
try:
|
|
421
|
+
current_json = doc.root.to_json()
|
|
422
|
+
updated_json = _apply_json_patch(current_json, patch_ops)
|
|
423
|
+
if not isinstance(updated_json, dict):
|
|
424
|
+
raise typer.BadParameter("Patch must produce a JSON object")
|
|
425
|
+
_apply_document_json(doc, updated_json)
|
|
426
|
+
print(f"[green]Updated document at {path}[/green]")
|
|
427
|
+
finally:
|
|
428
|
+
await client.sync.close(path=path)
|
|
429
|
+
except RoomException as exc:
|
|
430
|
+
print(f"[red]{exc}[/red]")
|
|
431
|
+
raise typer.Exit(1)
|
|
432
|
+
finally:
|
|
433
|
+
await client.__aexit__(None, None, None)
|
|
434
|
+
await account_client.close()
|