cortexctl 0.1.0.dev20260811135705__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.
- cortex_cli/__init__.py +1 -0
- cortex_cli/__main__.py +4 -0
- cortex_cli/client.py +1222 -0
- cortex_cli/config.py +88 -0
- cortex_cli/main.py +4878 -0
- cortex_cli/registry.py +862 -0
- cortexctl-0.1.0.dev20260811135705.dist-info/METADATA +213 -0
- cortexctl-0.1.0.dev20260811135705.dist-info/RECORD +10 -0
- cortexctl-0.1.0.dev20260811135705.dist-info/WHEEL +4 -0
- cortexctl-0.1.0.dev20260811135705.dist-info/entry_points.txt +2 -0
cortex_cli/client.py
ADDED
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
import secrets
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.error import HTTPError, URLError
|
|
11
|
+
from urllib.parse import urlencode, urlparse
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
13
|
+
|
|
14
|
+
DEFAULT_REQUEST_TIMEOUT_SECONDS = 30
|
|
15
|
+
MULTIPART_READ_CHUNK_SIZE = 1024 * 1024
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MultipartBody:
|
|
19
|
+
"""Repeatable multipart body that streams the file from disk."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
prefix: bytes,
|
|
25
|
+
path: Path,
|
|
26
|
+
suffix: bytes,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.prefix = prefix
|
|
29
|
+
self.path = path
|
|
30
|
+
self.suffix = suffix
|
|
31
|
+
self.file_size = path.stat().st_size
|
|
32
|
+
self.content_length = len(prefix) + self.file_size + len(suffix)
|
|
33
|
+
|
|
34
|
+
def __iter__(self) -> Iterator[bytes]:
|
|
35
|
+
yield self.prefix
|
|
36
|
+
with self.path.open("rb") as fileobj:
|
|
37
|
+
while chunk := fileobj.read(MULTIPART_READ_CHUNK_SIZE):
|
|
38
|
+
yield chunk
|
|
39
|
+
yield self.suffix
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CliAPIError(Exception):
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
code: str,
|
|
47
|
+
message: str,
|
|
48
|
+
status_code: int,
|
|
49
|
+
details: dict[str, Any] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.code = code
|
|
52
|
+
self.message = message
|
|
53
|
+
self.status_code = status_code
|
|
54
|
+
self.details = details or {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CortexClient:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
base_url: str,
|
|
62
|
+
access_token: str | None = None,
|
|
63
|
+
refresh_token: str | None = None,
|
|
64
|
+
token_updater: Callable[[dict[str, Any]], None] | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.base_url = base_url.rstrip("/")
|
|
67
|
+
self.access_token = access_token
|
|
68
|
+
self.refresh_token = refresh_token
|
|
69
|
+
self.token_updater = token_updater
|
|
70
|
+
|
|
71
|
+
def debug_login(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
email: str,
|
|
75
|
+
password: str | None,
|
|
76
|
+
display_name: str | None,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
return self.post(
|
|
79
|
+
"/auth/login",
|
|
80
|
+
{
|
|
81
|
+
"email": email,
|
|
82
|
+
"password": password,
|
|
83
|
+
"display_name": display_name,
|
|
84
|
+
},
|
|
85
|
+
authenticated=False,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def feishu_cli_authorize(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
cli_redirect_uri: str,
|
|
92
|
+
state: str,
|
|
93
|
+
code_challenge: str,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
return self.post(
|
|
96
|
+
"/auth/feishu/cli/authorize",
|
|
97
|
+
{
|
|
98
|
+
"cli_redirect_uri": cli_redirect_uri,
|
|
99
|
+
"state": state,
|
|
100
|
+
"code_challenge": code_challenge,
|
|
101
|
+
},
|
|
102
|
+
authenticated=False,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def feishu_cli_token(
|
|
106
|
+
self,
|
|
107
|
+
*,
|
|
108
|
+
cli_auth_code: str,
|
|
109
|
+
code_verifier: str,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
return self.post(
|
|
112
|
+
"/auth/feishu/cli/token",
|
|
113
|
+
{
|
|
114
|
+
"cli_auth_code": cli_auth_code,
|
|
115
|
+
"code_verifier": code_verifier,
|
|
116
|
+
},
|
|
117
|
+
authenticated=False,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def device_login_start(self) -> dict[str, Any]:
|
|
121
|
+
return self.post("/auth/device/start", {}, authenticated=False)
|
|
122
|
+
|
|
123
|
+
def device_login_token(self, *, device_code: str) -> dict[str, Any]:
|
|
124
|
+
return self.post(
|
|
125
|
+
"/auth/device/token",
|
|
126
|
+
{"device_code": device_code},
|
|
127
|
+
authenticated=False,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def me(self) -> dict[str, Any]:
|
|
131
|
+
return self.get("/me")
|
|
132
|
+
|
|
133
|
+
def health(self) -> dict[str, Any]:
|
|
134
|
+
return self.get("/health", authenticated=False)
|
|
135
|
+
|
|
136
|
+
def ready(self) -> dict[str, Any]:
|
|
137
|
+
return self.get("/ready", authenticated=False)
|
|
138
|
+
|
|
139
|
+
def workspace_panel(self) -> dict[str, Any]:
|
|
140
|
+
return self.get("/me/workspace-panel")
|
|
141
|
+
|
|
142
|
+
def create_workspace(self, *, name: str, description: str | None) -> dict[str, Any]:
|
|
143
|
+
return self.post("/workspaces", {"name": name, "description": description})
|
|
144
|
+
|
|
145
|
+
def get_workspace(self, workspace_id: str) -> dict[str, Any]:
|
|
146
|
+
return self.get(f"/workspaces/{workspace_id}")
|
|
147
|
+
|
|
148
|
+
def update_workspace(
|
|
149
|
+
self,
|
|
150
|
+
workspace_id: str,
|
|
151
|
+
*,
|
|
152
|
+
name: str | None,
|
|
153
|
+
description: str | None,
|
|
154
|
+
) -> dict[str, Any]:
|
|
155
|
+
return self.patch(
|
|
156
|
+
f"/workspaces/{workspace_id}",
|
|
157
|
+
{"name": name, "description": description},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def delete_workspace(self, workspace_id: str) -> None:
|
|
161
|
+
self.delete(f"/workspaces/{workspace_id}")
|
|
162
|
+
|
|
163
|
+
def list_invites(
|
|
164
|
+
self,
|
|
165
|
+
workspace_id: str,
|
|
166
|
+
*,
|
|
167
|
+
page: int,
|
|
168
|
+
page_size: int,
|
|
169
|
+
query: str | None,
|
|
170
|
+
status: str | None,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
return self.get(
|
|
173
|
+
f"/workspaces/{workspace_id}/invites",
|
|
174
|
+
query=compact_query(
|
|
175
|
+
{
|
|
176
|
+
"page": page,
|
|
177
|
+
"page_size": page_size,
|
|
178
|
+
"query": query,
|
|
179
|
+
"status": status,
|
|
180
|
+
}
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def create_invite(
|
|
185
|
+
self,
|
|
186
|
+
workspace_id: str,
|
|
187
|
+
*,
|
|
188
|
+
email: str,
|
|
189
|
+
role: str,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
return self.post(
|
|
192
|
+
f"/workspaces/{workspace_id}/invites",
|
|
193
|
+
{"invite_email": email, "role": role},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def revoke_invite(self, workspace_id: str, invite_id: str) -> None:
|
|
197
|
+
self.delete(f"/workspaces/{workspace_id}/invites/{invite_id}")
|
|
198
|
+
|
|
199
|
+
def enter_invite(self, invite_id: str) -> dict[str, Any]:
|
|
200
|
+
return self.post(f"/workspace-invites/{invite_id}/enter", {})
|
|
201
|
+
|
|
202
|
+
def reject_invite(self, invite_id: str) -> dict[str, Any]:
|
|
203
|
+
return self.post(f"/workspace-invites/{invite_id}/reject", {})
|
|
204
|
+
|
|
205
|
+
def list_invite_links(
|
|
206
|
+
self,
|
|
207
|
+
workspace_id: str,
|
|
208
|
+
*,
|
|
209
|
+
page: int,
|
|
210
|
+
page_size: int,
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
return self.get(
|
|
213
|
+
f"/workspaces/{workspace_id}/invite-links",
|
|
214
|
+
query=compact_query({"page": page, "page_size": page_size}),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def create_invite_link(self, workspace_id: str, *, name: str) -> dict[str, Any]:
|
|
218
|
+
return self.post(f"/workspaces/{workspace_id}/invite-links", {"name": name})
|
|
219
|
+
|
|
220
|
+
def revoke_invite_link(self, workspace_id: str, link_id: str) -> None:
|
|
221
|
+
self.delete(f"/workspaces/{workspace_id}/invite-links/{link_id}")
|
|
222
|
+
|
|
223
|
+
def preview_invite_link(self, token_or_url: str) -> dict[str, Any]:
|
|
224
|
+
token = invite_link_token_from_value(token_or_url)
|
|
225
|
+
return self.get(f"/workspace-invite-links/{token}/preview", authenticated=False)
|
|
226
|
+
|
|
227
|
+
def enter_invite_link(self, token_or_url: str) -> dict[str, Any]:
|
|
228
|
+
token = invite_link_token_from_value(token_or_url)
|
|
229
|
+
return self.post(f"/workspace-invite-links/{token}/enter", {})
|
|
230
|
+
|
|
231
|
+
def list_members(
|
|
232
|
+
self,
|
|
233
|
+
workspace_id: str,
|
|
234
|
+
*,
|
|
235
|
+
page: int,
|
|
236
|
+
page_size: int,
|
|
237
|
+
query: str | None,
|
|
238
|
+
) -> dict[str, Any]:
|
|
239
|
+
return self.get(
|
|
240
|
+
f"/workspaces/{workspace_id}/members",
|
|
241
|
+
query=compact_query({"page": page, "page_size": page_size, "query": query}),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def update_member_role(
|
|
245
|
+
self,
|
|
246
|
+
workspace_id: str,
|
|
247
|
+
user_id: str,
|
|
248
|
+
*,
|
|
249
|
+
role: str,
|
|
250
|
+
) -> dict[str, Any]:
|
|
251
|
+
return self.patch(
|
|
252
|
+
f"/workspaces/{workspace_id}/members/{user_id}",
|
|
253
|
+
{"role": role},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def remove_member(self, workspace_id: str, user_id: str) -> None:
|
|
257
|
+
self.delete(f"/workspaces/{workspace_id}/members/{user_id}")
|
|
258
|
+
|
|
259
|
+
def leave_workspace(self, workspace_id: str) -> None:
|
|
260
|
+
self.post(f"/workspaces/{workspace_id}/leave", {})
|
|
261
|
+
|
|
262
|
+
def transfer_owner(
|
|
263
|
+
self,
|
|
264
|
+
workspace_id: str,
|
|
265
|
+
*,
|
|
266
|
+
new_owner_id: str,
|
|
267
|
+
) -> list[dict[str, Any]]:
|
|
268
|
+
return self.post(
|
|
269
|
+
f"/workspaces/{workspace_id}/transfer-owner",
|
|
270
|
+
{"new_owner_id": new_owner_id},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def list_knowledge_bases(
|
|
274
|
+
self,
|
|
275
|
+
workspace_id: str,
|
|
276
|
+
*,
|
|
277
|
+
page: int,
|
|
278
|
+
page_size: int,
|
|
279
|
+
query: str | None,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
return self.get(
|
|
282
|
+
f"/workspaces/{workspace_id}/knowledge-bases",
|
|
283
|
+
query=compact_query({"page": page, "page_size": page_size, "query": query}),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def create_knowledge_base(
|
|
287
|
+
self,
|
|
288
|
+
workspace_id: str,
|
|
289
|
+
*,
|
|
290
|
+
name: str,
|
|
291
|
+
description: str | None,
|
|
292
|
+
pipeline_id: str | None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
return self.post(
|
|
295
|
+
f"/workspaces/{workspace_id}/knowledge-bases",
|
|
296
|
+
{
|
|
297
|
+
"name": name,
|
|
298
|
+
"description": description,
|
|
299
|
+
"pipeline_id": pipeline_id,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def get_knowledge_base(self, workspace_id: str, kb_id: str) -> dict[str, Any]:
|
|
304
|
+
return self.get(f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}")
|
|
305
|
+
|
|
306
|
+
def update_knowledge_base(
|
|
307
|
+
self,
|
|
308
|
+
workspace_id: str,
|
|
309
|
+
kb_id: str,
|
|
310
|
+
payload: dict[str, Any],
|
|
311
|
+
) -> dict[str, Any]:
|
|
312
|
+
return self.patch(
|
|
313
|
+
f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}",
|
|
314
|
+
payload,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def delete_knowledge_base(self, workspace_id: str, kb_id: str) -> None:
|
|
318
|
+
self.delete(f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}")
|
|
319
|
+
|
|
320
|
+
def update_knowledge_base_order(
|
|
321
|
+
self,
|
|
322
|
+
workspace_id: str,
|
|
323
|
+
items: list[dict[str, Any]],
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
return self.put(
|
|
326
|
+
f"/workspaces/{workspace_id}/knowledge-bases/order",
|
|
327
|
+
{"items": items},
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def get_knowledge_base_permissions(
|
|
331
|
+
self,
|
|
332
|
+
workspace_id: str,
|
|
333
|
+
kb_id: str,
|
|
334
|
+
) -> dict[str, Any]:
|
|
335
|
+
return self.get(
|
|
336
|
+
f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}/permissions"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def update_knowledge_base_permissions(
|
|
340
|
+
self,
|
|
341
|
+
workspace_id: str,
|
|
342
|
+
kb_id: str,
|
|
343
|
+
*,
|
|
344
|
+
edit_policy: str,
|
|
345
|
+
editor_user_ids: list[str],
|
|
346
|
+
) -> dict[str, Any]:
|
|
347
|
+
return self.put(
|
|
348
|
+
f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}/permissions",
|
|
349
|
+
{
|
|
350
|
+
"edit_policy": edit_policy,
|
|
351
|
+
"editor_user_ids": editor_user_ids,
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def get_folder_tree(self, workspace_id: str, kb_id: str) -> list[dict[str, Any]]:
|
|
356
|
+
return self.get(
|
|
357
|
+
f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}/folders/tree"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def create_folder(
|
|
361
|
+
self,
|
|
362
|
+
workspace_id: str,
|
|
363
|
+
kb_id: str,
|
|
364
|
+
*,
|
|
365
|
+
name: str,
|
|
366
|
+
parent_id: str | None,
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
return self.post(
|
|
369
|
+
f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}/folders",
|
|
370
|
+
{"name": name, "parent_id": parent_id},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def update_folder(
|
|
374
|
+
self,
|
|
375
|
+
workspace_id: str,
|
|
376
|
+
kb_id: str,
|
|
377
|
+
folder_id: str,
|
|
378
|
+
payload: dict[str, Any],
|
|
379
|
+
) -> dict[str, Any]:
|
|
380
|
+
return self.patch(
|
|
381
|
+
f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}/folders/{folder_id}",
|
|
382
|
+
payload,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def delete_folder(self, workspace_id: str, kb_id: str, folder_id: str) -> None:
|
|
386
|
+
self.delete(
|
|
387
|
+
f"/workspaces/{workspace_id}/knowledge-bases/{kb_id}/folders/{folder_id}"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def list_items(
|
|
391
|
+
self,
|
|
392
|
+
workspace_id: str,
|
|
393
|
+
*,
|
|
394
|
+
page: int,
|
|
395
|
+
page_size: int,
|
|
396
|
+
query: str | None,
|
|
397
|
+
is_available: bool | None,
|
|
398
|
+
is_locked: bool | None,
|
|
399
|
+
kb_id: str | None,
|
|
400
|
+
folder_id: str | None,
|
|
401
|
+
unclassified: bool,
|
|
402
|
+
) -> dict[str, Any]:
|
|
403
|
+
return self.get(
|
|
404
|
+
f"/workspaces/{workspace_id}/knowledge-items",
|
|
405
|
+
query=compact_query(
|
|
406
|
+
{
|
|
407
|
+
"page": page,
|
|
408
|
+
"page_size": page_size,
|
|
409
|
+
"query": query,
|
|
410
|
+
"is_available": is_available,
|
|
411
|
+
"is_locked": is_locked,
|
|
412
|
+
"kb_id": kb_id,
|
|
413
|
+
"folder_id": folder_id,
|
|
414
|
+
"unclassified": unclassified if unclassified else None,
|
|
415
|
+
}
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def create_item(
|
|
420
|
+
self,
|
|
421
|
+
workspace_id: str,
|
|
422
|
+
payload: dict[str, Any],
|
|
423
|
+
) -> dict[str, Any]:
|
|
424
|
+
return self.post(f"/workspaces/{workspace_id}/knowledge-items", payload)
|
|
425
|
+
|
|
426
|
+
def get_item(self, workspace_id: str, item_id: str) -> dict[str, Any]:
|
|
427
|
+
return self.get(f"/workspaces/{workspace_id}/knowledge-items/{item_id}")
|
|
428
|
+
|
|
429
|
+
def update_item(
|
|
430
|
+
self,
|
|
431
|
+
workspace_id: str,
|
|
432
|
+
item_id: str,
|
|
433
|
+
payload: dict[str, Any],
|
|
434
|
+
) -> dict[str, Any]:
|
|
435
|
+
return self.patch(
|
|
436
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}",
|
|
437
|
+
payload,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def delete_item(self, workspace_id: str, item_id: str) -> None:
|
|
441
|
+
self.delete(f"/workspaces/{workspace_id}/knowledge-items/{item_id}")
|
|
442
|
+
|
|
443
|
+
def replace_item_metadata(
|
|
444
|
+
self,
|
|
445
|
+
workspace_id: str,
|
|
446
|
+
item_id: str,
|
|
447
|
+
metadata: list[dict[str, Any]],
|
|
448
|
+
) -> dict[str, Any]:
|
|
449
|
+
return self.put(
|
|
450
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}/metadata",
|
|
451
|
+
{"metadata": metadata},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def create_item_mount(
|
|
455
|
+
self,
|
|
456
|
+
workspace_id: str,
|
|
457
|
+
item_id: str,
|
|
458
|
+
*,
|
|
459
|
+
kb_id: str,
|
|
460
|
+
folder_id: str | None,
|
|
461
|
+
) -> dict[str, Any]:
|
|
462
|
+
return self.post(
|
|
463
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}/mounts",
|
|
464
|
+
{"kb_id": kb_id, "folder_id": folder_id},
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def delete_item_mount(
|
|
468
|
+
self,
|
|
469
|
+
workspace_id: str,
|
|
470
|
+
item_id: str,
|
|
471
|
+
*,
|
|
472
|
+
kb_id: str,
|
|
473
|
+
folder_id: str | None,
|
|
474
|
+
) -> dict[str, Any]:
|
|
475
|
+
return self.delete_with_payload(
|
|
476
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}/mounts",
|
|
477
|
+
{"kb_id": kb_id, "folder_id": folder_id},
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
def list_trash_entries(
|
|
481
|
+
self,
|
|
482
|
+
workspace_id: str,
|
|
483
|
+
*,
|
|
484
|
+
page: int,
|
|
485
|
+
page_size: int,
|
|
486
|
+
) -> dict[str, Any]:
|
|
487
|
+
return self.get(
|
|
488
|
+
f"/workspaces/{workspace_id}/trash",
|
|
489
|
+
query=compact_query({"page": page, "page_size": page_size}),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def restore_trash_entry(
|
|
493
|
+
self,
|
|
494
|
+
workspace_id: str,
|
|
495
|
+
entry_id: str,
|
|
496
|
+
*,
|
|
497
|
+
target: dict[str, Any],
|
|
498
|
+
) -> dict[str, Any]:
|
|
499
|
+
return self.post(
|
|
500
|
+
f"/workspaces/{workspace_id}/trash/{entry_id}/restore",
|
|
501
|
+
{"target": target},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def purge_trash_entry(self, workspace_id: str, entry_id: str) -> None:
|
|
505
|
+
self.delete(f"/workspaces/{workspace_id}/trash/{entry_id}")
|
|
506
|
+
|
|
507
|
+
def discard_trash_entry(self, workspace_id: str, entry_id: str) -> None:
|
|
508
|
+
self.purge_trash_entry(workspace_id, entry_id)
|
|
509
|
+
|
|
510
|
+
def upload_knowledge_item(
|
|
511
|
+
self,
|
|
512
|
+
workspace_id: str,
|
|
513
|
+
*,
|
|
514
|
+
path: Path,
|
|
515
|
+
title: str | None,
|
|
516
|
+
kb_id: str | None,
|
|
517
|
+
folder_id: str | None,
|
|
518
|
+
relation_type: str,
|
|
519
|
+
pipeline_id: str | None,
|
|
520
|
+
metadata: list[dict[str, Any]],
|
|
521
|
+
idempotency_key: str | None,
|
|
522
|
+
) -> dict[str, Any]:
|
|
523
|
+
fields: dict[str, Any] = {
|
|
524
|
+
"title": title,
|
|
525
|
+
"kb_id": kb_id,
|
|
526
|
+
"folder_id": folder_id,
|
|
527
|
+
"relation_type": relation_type,
|
|
528
|
+
"pipeline_id": pipeline_id,
|
|
529
|
+
"metadata": json.dumps(metadata) if metadata else None,
|
|
530
|
+
}
|
|
531
|
+
return require_json_payload(
|
|
532
|
+
self.multipart_post(
|
|
533
|
+
f"/workspaces/{workspace_id}/uploads/knowledge-item",
|
|
534
|
+
fields=compact_query(fields),
|
|
535
|
+
file_field="file",
|
|
536
|
+
file_path=path,
|
|
537
|
+
idempotency_key=idempotency_key,
|
|
538
|
+
),
|
|
539
|
+
operation="file.upload",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
def add_file(
|
|
543
|
+
self,
|
|
544
|
+
workspace_id: str,
|
|
545
|
+
item_id: str,
|
|
546
|
+
*,
|
|
547
|
+
path: Path,
|
|
548
|
+
relation_type: str,
|
|
549
|
+
idempotency_key: str | None,
|
|
550
|
+
) -> dict[str, Any]:
|
|
551
|
+
return require_json_payload(
|
|
552
|
+
self.multipart_post(
|
|
553
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}/files",
|
|
554
|
+
fields={"relation_type": relation_type},
|
|
555
|
+
file_field="file",
|
|
556
|
+
file_path=path,
|
|
557
|
+
idempotency_key=idempotency_key,
|
|
558
|
+
),
|
|
559
|
+
operation="file.add",
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
def list_files(self, workspace_id: str, item_id: str) -> list[dict[str, Any]]:
|
|
563
|
+
return self.get(f"/workspaces/{workspace_id}/knowledge-items/{item_id}/files")
|
|
564
|
+
|
|
565
|
+
def download_file(self, workspace_id: str, file_id: str) -> bytes:
|
|
566
|
+
return self.request(
|
|
567
|
+
"GET",
|
|
568
|
+
f"/workspaces/{workspace_id}/files/{file_id}/download",
|
|
569
|
+
parse_json=False,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def preview_file(self, workspace_id: str, file_id: str) -> dict[str, Any]:
|
|
573
|
+
return self.get(f"/workspaces/{workspace_id}/files/{file_id}/preview")
|
|
574
|
+
|
|
575
|
+
def read_file(
|
|
576
|
+
self,
|
|
577
|
+
workspace_id: str,
|
|
578
|
+
file_id: str,
|
|
579
|
+
*,
|
|
580
|
+
format_: str,
|
|
581
|
+
range_: str | None,
|
|
582
|
+
source: str,
|
|
583
|
+
max_chars: int | None,
|
|
584
|
+
) -> dict[str, Any]:
|
|
585
|
+
return self.get(
|
|
586
|
+
f"/workspaces/{workspace_id}/files/{file_id}/read",
|
|
587
|
+
query=compact_query(
|
|
588
|
+
{
|
|
589
|
+
"format": format_,
|
|
590
|
+
"range": range_,
|
|
591
|
+
"source": source,
|
|
592
|
+
"max_chars": max_chars,
|
|
593
|
+
}
|
|
594
|
+
),
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
def delete_file(self, workspace_id: str, item_id: str, file_id: str) -> None:
|
|
598
|
+
self.delete(
|
|
599
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}/files/{file_id}"
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
def list_pipelines(self, workspace_id: str) -> list[dict[str, Any]]:
|
|
603
|
+
return self.get(f"/workspaces/{workspace_id}/processing-pipelines")
|
|
604
|
+
|
|
605
|
+
def create_pipeline(
|
|
606
|
+
self,
|
|
607
|
+
workspace_id: str,
|
|
608
|
+
*,
|
|
609
|
+
name: str,
|
|
610
|
+
goal: str,
|
|
611
|
+
config: dict[str, Any],
|
|
612
|
+
is_default: bool,
|
|
613
|
+
) -> dict[str, Any]:
|
|
614
|
+
return self.post(
|
|
615
|
+
f"/workspaces/{workspace_id}/processing-pipelines",
|
|
616
|
+
{
|
|
617
|
+
"name": name,
|
|
618
|
+
"goal": goal,
|
|
619
|
+
"config": config,
|
|
620
|
+
"is_default": is_default,
|
|
621
|
+
},
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
def get_pipeline(self, workspace_id: str, pipeline_id: str) -> dict[str, Any]:
|
|
625
|
+
return self.get(
|
|
626
|
+
f"/workspaces/{workspace_id}/processing-pipelines/{pipeline_id}"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def update_pipeline(
|
|
630
|
+
self,
|
|
631
|
+
workspace_id: str,
|
|
632
|
+
pipeline_id: str,
|
|
633
|
+
payload: dict[str, Any],
|
|
634
|
+
) -> dict[str, Any]:
|
|
635
|
+
return self.patch(
|
|
636
|
+
f"/workspaces/{workspace_id}/processing-pipelines/{pipeline_id}",
|
|
637
|
+
payload,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
def delete_pipeline(self, workspace_id: str, pipeline_id: str) -> None:
|
|
641
|
+
self.delete(f"/workspaces/{workspace_id}/processing-pipelines/{pipeline_id}")
|
|
642
|
+
|
|
643
|
+
def trigger_processing(
|
|
644
|
+
self,
|
|
645
|
+
workspace_id: str,
|
|
646
|
+
item_id: str,
|
|
647
|
+
*,
|
|
648
|
+
pipeline_id: str | None,
|
|
649
|
+
reason: str,
|
|
650
|
+
idempotency_key: str | None,
|
|
651
|
+
) -> dict[str, Any]:
|
|
652
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
653
|
+
return self.request(
|
|
654
|
+
"POST",
|
|
655
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}/processing-runs",
|
|
656
|
+
payload={"pipeline_id": pipeline_id, "reason": reason},
|
|
657
|
+
extra_headers=headers,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def list_processing_runs(
|
|
661
|
+
self,
|
|
662
|
+
workspace_id: str,
|
|
663
|
+
item_id: str,
|
|
664
|
+
) -> dict[str, Any]:
|
|
665
|
+
return self.get(
|
|
666
|
+
f"/workspaces/{workspace_id}/knowledge-items/{item_id}/processing-runs"
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def get_processing_run(self, workspace_id: str, run_id: str) -> dict[str, Any]:
|
|
670
|
+
return self.get(f"/workspaces/{workspace_id}/processing-runs/{run_id}")
|
|
671
|
+
|
|
672
|
+
def retry_processing_run(self, workspace_id: str, run_id: str) -> dict[str, Any]:
|
|
673
|
+
return self.post(
|
|
674
|
+
f"/workspaces/{workspace_id}/processing-runs/{run_id}/retry",
|
|
675
|
+
{},
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
def cancel_processing_run(self, workspace_id: str, run_id: str) -> dict[str, Any]:
|
|
679
|
+
return self.post(
|
|
680
|
+
f"/workspaces/{workspace_id}/processing-runs/{run_id}/cancel",
|
|
681
|
+
{},
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def reconcile_processing_runs(
|
|
685
|
+
self,
|
|
686
|
+
workspace_id: str,
|
|
687
|
+
*,
|
|
688
|
+
status: list[str] | None,
|
|
689
|
+
) -> list[dict[str, Any]]:
|
|
690
|
+
return self.post(
|
|
691
|
+
f"/workspaces/{workspace_id}/processing-runs/reconcile",
|
|
692
|
+
{"status": status},
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
def search_grep(
|
|
696
|
+
self,
|
|
697
|
+
workspace_id: str,
|
|
698
|
+
*,
|
|
699
|
+
query: str,
|
|
700
|
+
page: int,
|
|
701
|
+
page_size: int,
|
|
702
|
+
) -> dict[str, Any]:
|
|
703
|
+
return self.get(
|
|
704
|
+
f"/workspaces/{workspace_id}/search",
|
|
705
|
+
query=compact_query({"query": query, "page": page, "page_size": page_size}),
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def search_advanced(
|
|
709
|
+
self,
|
|
710
|
+
workspace_id: str,
|
|
711
|
+
*,
|
|
712
|
+
payload: dict[str, Any],
|
|
713
|
+
) -> dict[str, Any]:
|
|
714
|
+
return self.post(f"/workspaces/{workspace_id}/search/advanced", payload)
|
|
715
|
+
|
|
716
|
+
def search_semantic(
|
|
717
|
+
self,
|
|
718
|
+
workspace_id: str,
|
|
719
|
+
*,
|
|
720
|
+
query: str,
|
|
721
|
+
kb_ids: list[str] | None,
|
|
722
|
+
top_k: int,
|
|
723
|
+
recall: bool = False,
|
|
724
|
+
) -> list[dict[str, Any]]:
|
|
725
|
+
path = "recall" if recall else "search/semantic"
|
|
726
|
+
return self.post(
|
|
727
|
+
f"/workspaces/{workspace_id}/{path}",
|
|
728
|
+
{"query": query, "kb_ids": kb_ids, "top_k": top_k},
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def get_chunk(
|
|
732
|
+
self,
|
|
733
|
+
workspace_id: str,
|
|
734
|
+
file_id: str,
|
|
735
|
+
chunk_idx: int,
|
|
736
|
+
) -> dict[str, Any]:
|
|
737
|
+
return self.get(
|
|
738
|
+
f"/workspaces/{workspace_id}/files/{file_id}/chunks/{chunk_idx}"
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
def get_chunk_context(
|
|
742
|
+
self,
|
|
743
|
+
workspace_id: str,
|
|
744
|
+
file_id: str,
|
|
745
|
+
chunk_idx: int,
|
|
746
|
+
*,
|
|
747
|
+
window: int,
|
|
748
|
+
) -> dict[str, Any]:
|
|
749
|
+
return self.get(
|
|
750
|
+
f"/workspaces/{workspace_id}/files/{file_id}/chunks/{chunk_idx}/context",
|
|
751
|
+
query=compact_query({"window": window}),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
def locate_source(
|
|
755
|
+
self,
|
|
756
|
+
workspace_id: str,
|
|
757
|
+
file_id: str,
|
|
758
|
+
chunk_idx: int,
|
|
759
|
+
) -> dict[str, Any]:
|
|
760
|
+
return self.get(
|
|
761
|
+
f"/workspaces/{workspace_id}/files/{file_id}/chunks/{chunk_idx}/source"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
def expand_source(
|
|
765
|
+
self,
|
|
766
|
+
workspace_id: str,
|
|
767
|
+
file_id: str,
|
|
768
|
+
chunk_idx: int,
|
|
769
|
+
*,
|
|
770
|
+
mode: str,
|
|
771
|
+
window: int,
|
|
772
|
+
) -> dict[str, Any]:
|
|
773
|
+
return self.get(
|
|
774
|
+
f"/workspaces/{workspace_id}/files/{file_id}/chunks/"
|
|
775
|
+
f"{chunk_idx}/source/expand",
|
|
776
|
+
query=compact_query({"mode": mode, "window": window}),
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
def read_source_page(
|
|
780
|
+
self,
|
|
781
|
+
workspace_id: str,
|
|
782
|
+
file_id: str,
|
|
783
|
+
page_index: int,
|
|
784
|
+
*,
|
|
785
|
+
include_image: bool,
|
|
786
|
+
max_width: int,
|
|
787
|
+
include_bytes: bool = False,
|
|
788
|
+
) -> dict[str, Any]:
|
|
789
|
+
return self.get(
|
|
790
|
+
f"/workspaces/{workspace_id}/files/{file_id}/source/pages/{page_index}",
|
|
791
|
+
query=compact_query(
|
|
792
|
+
{
|
|
793
|
+
"include_image": include_image or include_bytes,
|
|
794
|
+
"include_bytes": include_bytes,
|
|
795
|
+
"max_width": max_width,
|
|
796
|
+
}
|
|
797
|
+
),
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
def read_region_image(
|
|
801
|
+
self,
|
|
802
|
+
workspace_id: str,
|
|
803
|
+
file_id: str,
|
|
804
|
+
*,
|
|
805
|
+
page_index: int,
|
|
806
|
+
bbox: list[float],
|
|
807
|
+
max_width: int,
|
|
808
|
+
include_bytes: bool = False,
|
|
809
|
+
) -> dict[str, Any]:
|
|
810
|
+
return self.post(
|
|
811
|
+
f"/workspaces/{workspace_id}/files/{file_id}/source/region-image",
|
|
812
|
+
{
|
|
813
|
+
"page_index": page_index,
|
|
814
|
+
"bbox": bbox,
|
|
815
|
+
"coordinate_space": "pdf_points",
|
|
816
|
+
"max_width": max_width,
|
|
817
|
+
"include_bytes": include_bytes,
|
|
818
|
+
},
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
def list_mcp_interfaces(self, workspace_id: str) -> list[dict[str, Any]]:
|
|
822
|
+
return self.get(f"/workspaces/{workspace_id}/mcp-interfaces")
|
|
823
|
+
|
|
824
|
+
def create_mcp_interface(
|
|
825
|
+
self,
|
|
826
|
+
workspace_id: str,
|
|
827
|
+
payload: dict[str, Any],
|
|
828
|
+
) -> dict[str, Any]:
|
|
829
|
+
return self.post(f"/workspaces/{workspace_id}/mcp-interfaces", payload)
|
|
830
|
+
|
|
831
|
+
def get_mcp_interface(self, workspace_id: str, mcp_id: str) -> dict[str, Any]:
|
|
832
|
+
return self.get(f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}")
|
|
833
|
+
|
|
834
|
+
def update_mcp_interface(
|
|
835
|
+
self,
|
|
836
|
+
workspace_id: str,
|
|
837
|
+
mcp_id: str,
|
|
838
|
+
payload: dict[str, Any],
|
|
839
|
+
) -> dict[str, Any]:
|
|
840
|
+
return self.patch(
|
|
841
|
+
f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}",
|
|
842
|
+
payload,
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
def enable_mcp_interface(self, workspace_id: str, mcp_id: str) -> dict[str, Any]:
|
|
846
|
+
return self.post(
|
|
847
|
+
f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}/enable",
|
|
848
|
+
{},
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
def disable_mcp_interface(self, workspace_id: str, mcp_id: str) -> dict[str, Any]:
|
|
852
|
+
return self.post(
|
|
853
|
+
f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}/disable",
|
|
854
|
+
{},
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def delete_mcp_interface(self, workspace_id: str, mcp_id: str) -> None:
|
|
858
|
+
self.delete(f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}")
|
|
859
|
+
|
|
860
|
+
def list_mcp_api_keys(
|
|
861
|
+
self,
|
|
862
|
+
workspace_id: str,
|
|
863
|
+
mcp_id: str,
|
|
864
|
+
) -> list[dict[str, Any]]:
|
|
865
|
+
return self.get(f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}/api-keys")
|
|
866
|
+
|
|
867
|
+
def create_mcp_api_key(
|
|
868
|
+
self,
|
|
869
|
+
workspace_id: str,
|
|
870
|
+
mcp_id: str,
|
|
871
|
+
*,
|
|
872
|
+
name: str | None,
|
|
873
|
+
) -> dict[str, Any]:
|
|
874
|
+
return self.post(
|
|
875
|
+
f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}/api-keys",
|
|
876
|
+
{"name": name},
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
def revoke_mcp_api_key(
|
|
880
|
+
self,
|
|
881
|
+
workspace_id: str,
|
|
882
|
+
mcp_id: str,
|
|
883
|
+
key_id: str,
|
|
884
|
+
) -> None:
|
|
885
|
+
self.delete(
|
|
886
|
+
f"/workspaces/{workspace_id}/mcp-interfaces/{mcp_id}/api-keys/{key_id}"
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
def gc_workspace(
|
|
890
|
+
self,
|
|
891
|
+
workspace_id: str,
|
|
892
|
+
*,
|
|
893
|
+
dry_run: bool,
|
|
894
|
+
include_vectors: bool,
|
|
895
|
+
) -> dict[str, Any]:
|
|
896
|
+
return self.post(
|
|
897
|
+
f"/workspaces/{workspace_id}/gc/workspace",
|
|
898
|
+
{"dry_run": dry_run, "include_vectors": include_vectors},
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
def gc_workspaces_deleted_before(
|
|
902
|
+
self,
|
|
903
|
+
*,
|
|
904
|
+
deleted_before: str,
|
|
905
|
+
dry_run: bool,
|
|
906
|
+
include_vectors: bool,
|
|
907
|
+
) -> dict[str, Any]:
|
|
908
|
+
return self.post(
|
|
909
|
+
"/gc/workspaces",
|
|
910
|
+
{
|
|
911
|
+
"deleted_before": deleted_before,
|
|
912
|
+
"dry_run": dry_run,
|
|
913
|
+
"include_vectors": include_vectors,
|
|
914
|
+
},
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
def gc_item(
|
|
918
|
+
self,
|
|
919
|
+
workspace_id: str,
|
|
920
|
+
item_id: str,
|
|
921
|
+
*,
|
|
922
|
+
dry_run: bool,
|
|
923
|
+
include_vectors: bool,
|
|
924
|
+
) -> dict[str, Any]:
|
|
925
|
+
return self.post(
|
|
926
|
+
f"/workspaces/{workspace_id}/gc/items/{item_id}",
|
|
927
|
+
{"dry_run": dry_run, "include_vectors": include_vectors},
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
def gc_mcp_call_logs(
|
|
931
|
+
self,
|
|
932
|
+
*,
|
|
933
|
+
deleted_before: str,
|
|
934
|
+
dry_run: bool,
|
|
935
|
+
workspace_id: str | None = None,
|
|
936
|
+
) -> dict[str, Any]:
|
|
937
|
+
return self.post(
|
|
938
|
+
"/gc/mcp-call-logs",
|
|
939
|
+
{
|
|
940
|
+
"deleted_before": deleted_before,
|
|
941
|
+
"dry_run": dry_run,
|
|
942
|
+
"workspace_id": workspace_id,
|
|
943
|
+
},
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
def get_vector_index_status(self, workspace_id: str) -> dict[str, Any]:
|
|
947
|
+
payload = self.get(f"/workspaces/{workspace_id}/vector-index")
|
|
948
|
+
status = str(
|
|
949
|
+
payload.get("status") or payload.get("vector_index_status") or "unknown"
|
|
950
|
+
)
|
|
951
|
+
return {
|
|
952
|
+
**payload,
|
|
953
|
+
"workspace_id": payload.get("workspace_id") or workspace_id,
|
|
954
|
+
"vector_index_status": status,
|
|
955
|
+
"needs_rebuild": status == "needs_rebuild",
|
|
956
|
+
"vector_index_error": payload.get("error")
|
|
957
|
+
or payload.get("vector_index_error"),
|
|
958
|
+
"vector_index_rebuild_requested_at": payload.get("rebuild_requested_at")
|
|
959
|
+
or payload.get(
|
|
960
|
+
"vector_index_rebuild_requested_at",
|
|
961
|
+
),
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
def rebuild_vector_index(
|
|
965
|
+
self,
|
|
966
|
+
workspace_id: str,
|
|
967
|
+
*,
|
|
968
|
+
dry_run: bool,
|
|
969
|
+
) -> dict[str, Any]:
|
|
970
|
+
return self.post(
|
|
971
|
+
f"/workspaces/{workspace_id}/vector-index/rebuild",
|
|
972
|
+
{"dry_run": dry_run},
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
def revoke(self, refresh_token: str) -> None:
|
|
976
|
+
self.post(
|
|
977
|
+
"/auth/revoke",
|
|
978
|
+
{"refresh_token": refresh_token},
|
|
979
|
+
authenticated=False,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
def refresh(self, refresh_token: str) -> dict[str, Any]:
|
|
983
|
+
return self.post(
|
|
984
|
+
"/auth/refresh",
|
|
985
|
+
{"refresh_token": refresh_token},
|
|
986
|
+
authenticated=False,
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
def get(
|
|
990
|
+
self,
|
|
991
|
+
path: str,
|
|
992
|
+
query: dict[str, Any] | None = None,
|
|
993
|
+
*,
|
|
994
|
+
authenticated: bool = True,
|
|
995
|
+
) -> Any:
|
|
996
|
+
return self.request("GET", path, query=query, authenticated=authenticated)
|
|
997
|
+
|
|
998
|
+
def post(
|
|
999
|
+
self,
|
|
1000
|
+
path: str,
|
|
1001
|
+
payload: dict[str, Any],
|
|
1002
|
+
*,
|
|
1003
|
+
authenticated: bool = True,
|
|
1004
|
+
) -> Any:
|
|
1005
|
+
return self.request("POST", path, payload=payload, authenticated=authenticated)
|
|
1006
|
+
|
|
1007
|
+
def patch(self, path: str, payload: dict[str, Any]) -> Any:
|
|
1008
|
+
return self.request("PATCH", path, payload=payload)
|
|
1009
|
+
|
|
1010
|
+
def put(self, path: str, payload: dict[str, Any]) -> Any:
|
|
1011
|
+
return self.request("PUT", path, payload=payload)
|
|
1012
|
+
|
|
1013
|
+
def delete(self, path: str) -> Any:
|
|
1014
|
+
return self.request("DELETE", path)
|
|
1015
|
+
|
|
1016
|
+
def delete_with_payload(self, path: str, payload: dict[str, Any]) -> Any:
|
|
1017
|
+
return self.request("DELETE", path, payload=payload)
|
|
1018
|
+
|
|
1019
|
+
def request(
|
|
1020
|
+
self,
|
|
1021
|
+
method: str,
|
|
1022
|
+
path: str,
|
|
1023
|
+
*,
|
|
1024
|
+
payload: dict[str, Any] | None = None,
|
|
1025
|
+
query: dict[str, Any] | None = None,
|
|
1026
|
+
authenticated: bool = True,
|
|
1027
|
+
retry_on_unauthorized: bool = True,
|
|
1028
|
+
raw_body: bytes | MultipartBody | None = None,
|
|
1029
|
+
content_type: str | None = None,
|
|
1030
|
+
extra_headers: dict[str, str] | None = None,
|
|
1031
|
+
parse_json: bool = True,
|
|
1032
|
+
) -> Any:
|
|
1033
|
+
url = f"{self.base_url}{path}"
|
|
1034
|
+
if query:
|
|
1035
|
+
url = f"{url}?{urlencode(query)}"
|
|
1036
|
+
|
|
1037
|
+
headers = {
|
|
1038
|
+
"Accept": "application/json",
|
|
1039
|
+
"X-Request-ID": secrets.token_hex(16),
|
|
1040
|
+
}
|
|
1041
|
+
body = None
|
|
1042
|
+
if payload is not None:
|
|
1043
|
+
body = json.dumps(payload).encode("utf-8")
|
|
1044
|
+
headers["Content-Type"] = "application/json"
|
|
1045
|
+
if raw_body is not None:
|
|
1046
|
+
body = raw_body
|
|
1047
|
+
if content_type is not None:
|
|
1048
|
+
headers["Content-Type"] = content_type
|
|
1049
|
+
if isinstance(raw_body, MultipartBody):
|
|
1050
|
+
headers["Content-Length"] = str(raw_body.content_length)
|
|
1051
|
+
if extra_headers:
|
|
1052
|
+
headers.update(extra_headers)
|
|
1053
|
+
if authenticated:
|
|
1054
|
+
if not self.access_token:
|
|
1055
|
+
raise CliAPIError(
|
|
1056
|
+
code="auth.unauthorized",
|
|
1057
|
+
message="Not logged in. Run `cortex auth login`.",
|
|
1058
|
+
status_code=401,
|
|
1059
|
+
)
|
|
1060
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
1061
|
+
|
|
1062
|
+
request = Request(url, data=body, headers=headers, method=method)
|
|
1063
|
+
try:
|
|
1064
|
+
with urlopen(request, timeout=request_timeout_seconds()) as response:
|
|
1065
|
+
content = response.read()
|
|
1066
|
+
if not parse_json:
|
|
1067
|
+
return content
|
|
1068
|
+
if not content:
|
|
1069
|
+
return None
|
|
1070
|
+
return json.loads(content.decode("utf-8"))
|
|
1071
|
+
except HTTPError as exc:
|
|
1072
|
+
error = parse_http_error(exc)
|
|
1073
|
+
if (
|
|
1074
|
+
authenticated
|
|
1075
|
+
and retry_on_unauthorized
|
|
1076
|
+
and error.status_code == 401
|
|
1077
|
+
and self.refresh_token
|
|
1078
|
+
):
|
|
1079
|
+
self.refresh_access_token()
|
|
1080
|
+
return self.request(
|
|
1081
|
+
method,
|
|
1082
|
+
path,
|
|
1083
|
+
payload=payload,
|
|
1084
|
+
query=query,
|
|
1085
|
+
authenticated=authenticated,
|
|
1086
|
+
retry_on_unauthorized=False,
|
|
1087
|
+
raw_body=raw_body,
|
|
1088
|
+
content_type=content_type,
|
|
1089
|
+
extra_headers=extra_headers,
|
|
1090
|
+
parse_json=parse_json,
|
|
1091
|
+
)
|
|
1092
|
+
raise error from exc
|
|
1093
|
+
except URLError as exc:
|
|
1094
|
+
raise CliAPIError(
|
|
1095
|
+
code="network.error",
|
|
1096
|
+
message=str(exc.reason),
|
|
1097
|
+
status_code=0,
|
|
1098
|
+
) from exc
|
|
1099
|
+
|
|
1100
|
+
def multipart_post(
|
|
1101
|
+
self,
|
|
1102
|
+
endpoint_path: str,
|
|
1103
|
+
*,
|
|
1104
|
+
fields: dict[str, Any],
|
|
1105
|
+
file_field: str,
|
|
1106
|
+
file_path: Path,
|
|
1107
|
+
idempotency_key: str | None,
|
|
1108
|
+
) -> Any:
|
|
1109
|
+
body, content_type = encode_multipart(
|
|
1110
|
+
fields=fields,
|
|
1111
|
+
file_field=file_field,
|
|
1112
|
+
path=file_path,
|
|
1113
|
+
)
|
|
1114
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
1115
|
+
return self.request(
|
|
1116
|
+
"POST",
|
|
1117
|
+
endpoint_path,
|
|
1118
|
+
raw_body=body,
|
|
1119
|
+
content_type=content_type,
|
|
1120
|
+
extra_headers=headers,
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
def refresh_access_token(self) -> None:
|
|
1124
|
+
if not self.refresh_token:
|
|
1125
|
+
raise CliAPIError(
|
|
1126
|
+
code="auth.unauthorized",
|
|
1127
|
+
message="Not logged in. Run `cortex auth login`.",
|
|
1128
|
+
status_code=401,
|
|
1129
|
+
)
|
|
1130
|
+
payload = self.refresh(self.refresh_token)
|
|
1131
|
+
self.access_token = payload["access_token"]
|
|
1132
|
+
self.refresh_token = payload["refresh_token"]
|
|
1133
|
+
if self.token_updater is not None:
|
|
1134
|
+
self.token_updater(payload)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def parse_http_error(exc: HTTPError) -> CliAPIError:
|
|
1138
|
+
try:
|
|
1139
|
+
payload = json.loads(exc.read().decode("utf-8"))
|
|
1140
|
+
error = payload.get("error", {})
|
|
1141
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
1142
|
+
error = {}
|
|
1143
|
+
|
|
1144
|
+
return CliAPIError(
|
|
1145
|
+
code=str(error.get("code") or "http.error"),
|
|
1146
|
+
message=str(error.get("message") or exc.reason),
|
|
1147
|
+
status_code=exc.code,
|
|
1148
|
+
details=error.get("details") or {},
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def request_timeout_seconds() -> int:
|
|
1153
|
+
raw = os.environ.get("CORTEX_CLI_REQUEST_TIMEOUT_SECONDS")
|
|
1154
|
+
if raw is None:
|
|
1155
|
+
return DEFAULT_REQUEST_TIMEOUT_SECONDS
|
|
1156
|
+
try:
|
|
1157
|
+
value = int(raw)
|
|
1158
|
+
except ValueError:
|
|
1159
|
+
return DEFAULT_REQUEST_TIMEOUT_SECONDS
|
|
1160
|
+
if value <= 0:
|
|
1161
|
+
return DEFAULT_REQUEST_TIMEOUT_SECONDS
|
|
1162
|
+
return value
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def compact_query(query: dict[str, Any]) -> dict[str, Any]:
|
|
1166
|
+
return {key: value for key, value in query.items() if value is not None}
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def invite_link_token_from_value(value: str) -> str:
|
|
1170
|
+
raw = value.strip()
|
|
1171
|
+
parsed = urlparse(raw)
|
|
1172
|
+
if parsed.scheme and parsed.netloc:
|
|
1173
|
+
path_token = parsed.path.rstrip("/").rsplit("/", 1)[-1]
|
|
1174
|
+
return path_token or raw
|
|
1175
|
+
return raw
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def require_json_payload(payload: Any, *, operation: str) -> dict[str, Any]:
|
|
1179
|
+
if isinstance(payload, dict):
|
|
1180
|
+
return payload
|
|
1181
|
+
raise CliAPIError(
|
|
1182
|
+
code="api.empty_response",
|
|
1183
|
+
message=f"{operation} expected a JSON response body.",
|
|
1184
|
+
status_code=502,
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def encode_multipart(
|
|
1189
|
+
*,
|
|
1190
|
+
fields: dict[str, Any],
|
|
1191
|
+
file_field: str,
|
|
1192
|
+
path: Path,
|
|
1193
|
+
) -> tuple[MultipartBody, str]:
|
|
1194
|
+
boundary = f"----cortex-{secrets.token_hex(16)}"
|
|
1195
|
+
chunks: list[bytes] = []
|
|
1196
|
+
for name, value in fields.items():
|
|
1197
|
+
chunks.extend(
|
|
1198
|
+
[
|
|
1199
|
+
f"--{boundary}\r\n".encode(),
|
|
1200
|
+
f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode(),
|
|
1201
|
+
str(value).encode(),
|
|
1202
|
+
b"\r\n",
|
|
1203
|
+
]
|
|
1204
|
+
)
|
|
1205
|
+
filename = path.name
|
|
1206
|
+
mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
|
1207
|
+
chunks.extend(
|
|
1208
|
+
[
|
|
1209
|
+
f"--{boundary}\r\n".encode(),
|
|
1210
|
+
(
|
|
1211
|
+
f'Content-Disposition: form-data; name="{file_field}"; '
|
|
1212
|
+
f'filename="{filename}"\r\n'
|
|
1213
|
+
).encode(),
|
|
1214
|
+
f"Content-Type: {mime_type}\r\n\r\n".encode(),
|
|
1215
|
+
]
|
|
1216
|
+
)
|
|
1217
|
+
body = MultipartBody(
|
|
1218
|
+
prefix=b"".join(chunks),
|
|
1219
|
+
path=path,
|
|
1220
|
+
suffix=b"\r\n" + f"--{boundary}--\r\n".encode(),
|
|
1221
|
+
)
|
|
1222
|
+
return body, f"multipart/form-data; boundary={boundary}"
|