meshagent-api 0.20.5__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/api/__init__.py +96 -0
- meshagent/api/chan.py +177 -0
- meshagent/api/client.py +2000 -0
- meshagent/api/crdt.py +479 -0
- meshagent/api/entrypoint.js +8 -0
- meshagent/api/helpers.py +103 -0
- meshagent/api/keys.py +109 -0
- meshagent/api/messaging.py +350 -0
- meshagent/api/oauth.py +17 -0
- meshagent/api/participant.py +17 -0
- meshagent/api/participant_token.py +503 -0
- meshagent/api/participant_token_test.py +200 -0
- meshagent/api/port_forward.py +161 -0
- meshagent/api/protocol.py +335 -0
- meshagent/api/protocol_test.py +54 -0
- meshagent/api/py.typed +0 -0
- meshagent/api/reasoning_schema.py +0 -0
- meshagent/api/room_server_client.py +3286 -0
- meshagent/api/runtime.py +356 -0
- meshagent/api/runtime_test.py +325 -0
- meshagent/api/schema.py +397 -0
- meshagent/api/schema_document.py +688 -0
- meshagent/api/schema_document_test.py +88 -0
- meshagent/api/schema_registry.py +75 -0
- meshagent/api/schema_test.py +397 -0
- meshagent/api/schema_util.py +58 -0
- meshagent/api/services.py +394 -0
- meshagent/api/specs/service.py +253 -0
- meshagent/api/token_test.py +107 -0
- meshagent/api/version.py +1 -0
- meshagent/api/webhooks.py +283 -0
- meshagent/api/websocket_protocol.py +126 -0
- meshagent_api-0.20.5.dist-info/METADATA +101 -0
- meshagent_api-0.20.5.dist-info/RECORD +37 -0
- meshagent_api-0.20.5.dist-info/WHEEL +5 -0
- meshagent_api-0.20.5.dist-info/licenses/LICENSE +201 -0
- meshagent_api-0.20.5.dist-info/top_level.txt +1 -0
meshagent/api/client.py
ADDED
|
@@ -0,0 +1,2000 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
from typing import Any, Dict, List, Optional, Literal, cast
|
|
3
|
+
from pydantic import BaseModel, ValidationError, JsonValue, Field, ConfigDict
|
|
4
|
+
from meshagent.api import RoomException
|
|
5
|
+
from meshagent.api.participant_token import ApiScope
|
|
6
|
+
from meshagent.api.helpers import meshagent_base_url
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from meshagent.api.specs.service import (
|
|
9
|
+
ServiceSpec,
|
|
10
|
+
)
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
# ------------------------------------------------------------------
|
|
14
|
+
# Secret models
|
|
15
|
+
# ------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NotFoundError(RoomException):
|
|
19
|
+
"""404 – resource does not exist."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PermissionDeniedError(RoomException):
|
|
23
|
+
"""403 – permission denied."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConflictError(RoomException):
|
|
27
|
+
"""409 – conflicting or duplicate resource."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValidationErrorResponse(RoomException):
|
|
31
|
+
"""400 – invalid request payload."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ServerError(RoomException):
|
|
35
|
+
"""5xx – server-side failure."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OAuthClient(BaseModel):
|
|
39
|
+
client_id: str
|
|
40
|
+
client_secret: str
|
|
41
|
+
grant_types: list[str]
|
|
42
|
+
response_types: list[str]
|
|
43
|
+
redirect_uris: list[str]
|
|
44
|
+
scope: str
|
|
45
|
+
project_id: str
|
|
46
|
+
metadata: dict[str, str]
|
|
47
|
+
official: bool
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RoomConnectionInfo(BaseModel):
|
|
51
|
+
jwt: str
|
|
52
|
+
room_name: str
|
|
53
|
+
project_id: str
|
|
54
|
+
room_url: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RoomShare(BaseModel):
|
|
58
|
+
id: str
|
|
59
|
+
project_id: str
|
|
60
|
+
settings: dict[str, JsonValue]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RoomShareConnectionInfo(BaseModel):
|
|
64
|
+
jwt: str
|
|
65
|
+
room_name: str
|
|
66
|
+
project_id: str
|
|
67
|
+
settings: dict[str, JsonValue]
|
|
68
|
+
room_url: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RoomSession(BaseModel):
|
|
72
|
+
id: str
|
|
73
|
+
room_id: Optional[str]
|
|
74
|
+
room_name: str
|
|
75
|
+
created_at: datetime
|
|
76
|
+
is_active: bool
|
|
77
|
+
participants: Optional[dict[str, int]] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _ListRoomSessionsResponse(BaseModel):
|
|
81
|
+
sessions: list[RoomSession]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Room(BaseModel):
|
|
85
|
+
id: str
|
|
86
|
+
name: str
|
|
87
|
+
metadata: dict[str, JsonValue]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ProjectRoomGrant(BaseModel):
|
|
91
|
+
room: Room
|
|
92
|
+
user_id: str
|
|
93
|
+
permissions: ApiScope
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ProjectRoomGrantCount(BaseModel):
|
|
97
|
+
room: Room
|
|
98
|
+
count: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ProjectUserGrantCount(BaseModel):
|
|
102
|
+
user_id: str
|
|
103
|
+
count: int
|
|
104
|
+
first_name: Optional[str] = None
|
|
105
|
+
last_name: Optional[str] = None
|
|
106
|
+
email: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _CreateRoomGrantRequest(BaseModel):
|
|
110
|
+
room_id: str
|
|
111
|
+
user_id: Optional[str] = None
|
|
112
|
+
email: Optional[str] = None
|
|
113
|
+
permissions: ApiScope
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _UpdateRoomGrantRequest(BaseModel):
|
|
117
|
+
room_id: str
|
|
118
|
+
user_id: str
|
|
119
|
+
permissions: ApiScope
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class _BaseSecret(BaseModel):
|
|
123
|
+
"""Common fields shared by all secrets."""
|
|
124
|
+
|
|
125
|
+
id: str
|
|
126
|
+
name: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class PullSecret(_BaseSecret):
|
|
130
|
+
"""
|
|
131
|
+
A Docker-registry credential.
|
|
132
|
+
|
|
133
|
+
When you call `model_dump() / dict()` this object produces the same
|
|
134
|
+
structure consumed by `map_secret_data("docker", …)` in the room
|
|
135
|
+
provisioner.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
type: Literal["docker"] = "docker"
|
|
139
|
+
|
|
140
|
+
server: str = Field(..., description="Registry host (e.g. registry-1.docker.io)")
|
|
141
|
+
username: str
|
|
142
|
+
password: str
|
|
143
|
+
email: str = Field(
|
|
144
|
+
"none@example.com",
|
|
145
|
+
description="Email is required by the Docker spec, but is unused",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def to_payload(self) -> Dict[str, str]:
|
|
149
|
+
return {
|
|
150
|
+
"server": self.server,
|
|
151
|
+
"username": self.username,
|
|
152
|
+
"password": self.password,
|
|
153
|
+
"email": self.email,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class KeysSecret(_BaseSecret):
|
|
158
|
+
"""
|
|
159
|
+
An *opaque* secret that will be exposed to containers as individual
|
|
160
|
+
environment variables.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
KeysSecret(
|
|
164
|
+
id="sec-123",
|
|
165
|
+
name="openai",
|
|
166
|
+
data={"OPENAI_API_KEY": "sk-...", "ORG": "myorg"}
|
|
167
|
+
)
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
type: Literal["keys"] = "keys"
|
|
171
|
+
data: Dict[str, str]
|
|
172
|
+
|
|
173
|
+
def to_payload(self) -> Dict[str, str]:
|
|
174
|
+
return self.data
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
SecretLike = PullSecret | KeysSecret
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_secret(raw: dict) -> SecretLike:
|
|
181
|
+
"""
|
|
182
|
+
Decide which concrete Pydantic class to use based on the 'type' field.
|
|
183
|
+
"""
|
|
184
|
+
if raw.get("type") == "docker":
|
|
185
|
+
return PullSecret.model_validate(
|
|
186
|
+
{"id": raw["id"], "name": raw["name"], "type": raw["type"], **raw["data"]}
|
|
187
|
+
)
|
|
188
|
+
else: # defaults to keys_secret
|
|
189
|
+
return KeysSecret.model_validate(
|
|
190
|
+
{
|
|
191
|
+
"id": raw["id"],
|
|
192
|
+
"name": raw["name"],
|
|
193
|
+
"type": raw["type"],
|
|
194
|
+
"data": raw["data"],
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
ProjectRole = Literal["member", "admin", "developer"]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class _CreateMailboxRequest(BaseModel):
|
|
203
|
+
room: str
|
|
204
|
+
queue: str
|
|
205
|
+
address: str
|
|
206
|
+
public: bool
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _UpdateMailboxRequest(BaseModel):
|
|
210
|
+
room: str
|
|
211
|
+
queue: str
|
|
212
|
+
public: bool
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Mailbox(BaseModel):
|
|
216
|
+
"""
|
|
217
|
+
Minimal shape returned by the server for a mailbox.
|
|
218
|
+
Extra fields (if any) from the server response will be ignored.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
address: str
|
|
222
|
+
room: str
|
|
223
|
+
queue: str
|
|
224
|
+
public: bool
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class Balance(BaseModel):
|
|
228
|
+
balance: float
|
|
229
|
+
auto_recharge_threshold: Optional[float] = Field(
|
|
230
|
+
default=None, alias="auto_recharge_threshold"
|
|
231
|
+
)
|
|
232
|
+
auto_recharge_amount: Optional[float] = Field(
|
|
233
|
+
default=None, alias="auto_recharge_amount"
|
|
234
|
+
)
|
|
235
|
+
last_recharge: Optional[datetime] = Field(default=None, alias="last_recharge")
|
|
236
|
+
|
|
237
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Transaction(BaseModel):
|
|
241
|
+
id: str
|
|
242
|
+
amount: float
|
|
243
|
+
reference: Optional[str] = None
|
|
244
|
+
reference_type: Optional[str] = Field(default=None, alias="referenceType")
|
|
245
|
+
description: str
|
|
246
|
+
created_at: datetime = Field(alias="created_at")
|
|
247
|
+
|
|
248
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ScheduledTask(BaseModel):
|
|
252
|
+
id: str
|
|
253
|
+
project_id: str
|
|
254
|
+
room_name: str
|
|
255
|
+
queue_name: str
|
|
256
|
+
payload: dict
|
|
257
|
+
schedule: str
|
|
258
|
+
active: bool
|
|
259
|
+
once: bool
|
|
260
|
+
annotations: dict[str, str]
|
|
261
|
+
|
|
262
|
+
last_run_id: Optional[int] = None
|
|
263
|
+
last_start_time: Optional[datetime] = None
|
|
264
|
+
last_end_time: Optional[datetime] = None
|
|
265
|
+
last_status: Optional[str] = None
|
|
266
|
+
last_return_message: Optional[str] = None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class _CreateScheduledTaskRequest(BaseModel):
|
|
270
|
+
id: Optional[str] = None
|
|
271
|
+
room_name: str
|
|
272
|
+
queue_name: str
|
|
273
|
+
payload: dict # dict or json-string
|
|
274
|
+
schedule: str
|
|
275
|
+
active: bool = True
|
|
276
|
+
once: bool = False
|
|
277
|
+
annotations: dict[str, str]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class _UpdateScheduledTaskRequest(BaseModel):
|
|
281
|
+
room_name: Optional[str] = None
|
|
282
|
+
queue_name: Optional[str] = None
|
|
283
|
+
payload: Optional[dict] = None # dict or json-string
|
|
284
|
+
schedule: Optional[str] = None
|
|
285
|
+
active: Optional[bool] = None
|
|
286
|
+
annotations: dict[str, str]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class _ListScheduledTasksResponse(BaseModel):
|
|
290
|
+
tasks: List[ScheduledTask]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class Meshagent:
|
|
294
|
+
"""
|
|
295
|
+
A simple asynchronous client to interact with the accounts routes.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(
|
|
299
|
+
self,
|
|
300
|
+
*,
|
|
301
|
+
base_url: str = meshagent_base_url(),
|
|
302
|
+
token: str = os.getenv("MESHAGENT_API_KEY"),
|
|
303
|
+
):
|
|
304
|
+
"""
|
|
305
|
+
:param base_url: The root URL of your server, e.g. 'http://localhost:8080'.
|
|
306
|
+
:param token: A Bearer token for the Authorization header.
|
|
307
|
+
"""
|
|
308
|
+
self.base_url = base_url.rstrip("/")
|
|
309
|
+
self.token = token # The "Bearer" token
|
|
310
|
+
|
|
311
|
+
self._session = aiohttp.ClientSession()
|
|
312
|
+
|
|
313
|
+
async def close(self):
|
|
314
|
+
if not self._session.closed:
|
|
315
|
+
await self._session.close()
|
|
316
|
+
|
|
317
|
+
async def __aenter__(self):
|
|
318
|
+
return self
|
|
319
|
+
|
|
320
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
321
|
+
await self.close()
|
|
322
|
+
|
|
323
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
324
|
+
"""
|
|
325
|
+
Returns the default headers including Bearer Authorization.
|
|
326
|
+
"""
|
|
327
|
+
return {
|
|
328
|
+
"Authorization": f"Bearer {self.token}",
|
|
329
|
+
"Content-Type": "application/json",
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async def _raise_for_status(
|
|
333
|
+
self,
|
|
334
|
+
resp: aiohttp.ClientResponse,
|
|
335
|
+
) -> None:
|
|
336
|
+
if resp.status < 400:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
body = await resp.text()
|
|
341
|
+
except Exception:
|
|
342
|
+
body = "<unable to read body>"
|
|
343
|
+
|
|
344
|
+
msg = f"Status={resp.status}, body={body}"
|
|
345
|
+
|
|
346
|
+
if resp.status == 404:
|
|
347
|
+
raise NotFoundError(msg)
|
|
348
|
+
if resp.status == 403:
|
|
349
|
+
raise PermissionDeniedError(msg)
|
|
350
|
+
if resp.status == 409:
|
|
351
|
+
raise ConflictError(msg)
|
|
352
|
+
if resp.status == 400:
|
|
353
|
+
raise ValidationErrorResponse(msg)
|
|
354
|
+
if resp.status >= 500:
|
|
355
|
+
raise ServerError(msg)
|
|
356
|
+
|
|
357
|
+
raise RoomException(msg)
|
|
358
|
+
|
|
359
|
+
async def _ensure_success(
|
|
360
|
+
self, resp: aiohttp.ClientResponse, *, action: str
|
|
361
|
+
) -> None:
|
|
362
|
+
if resp.status < 400:
|
|
363
|
+
return
|
|
364
|
+
try:
|
|
365
|
+
body = await resp.text()
|
|
366
|
+
except Exception:
|
|
367
|
+
body = "<unable to read body>"
|
|
368
|
+
raise RoomException(
|
|
369
|
+
f"Failed to {action}. Status code: {resp.status}, body: {body}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
async def upload(self, *, project_id: str, path: str, data: bytes) -> None:
|
|
373
|
+
"""Upload a file to project storage.
|
|
374
|
+
|
|
375
|
+
Corresponds to: **POST /projects/:project_id/storage/upload**
|
|
376
|
+
Query params: `path`
|
|
377
|
+
Body: raw binary data (bytes)
|
|
378
|
+
Raises RoomException on HTTP >= 400.
|
|
379
|
+
"""
|
|
380
|
+
url = f"{self.base_url}/projects/{project_id}/storage/upload"
|
|
381
|
+
params = {"path": path}
|
|
382
|
+
|
|
383
|
+
async with self._session.post(
|
|
384
|
+
url,
|
|
385
|
+
params=params,
|
|
386
|
+
headers={**self._get_headers(), "Content-Type": "application/octet-stream"},
|
|
387
|
+
data=data,
|
|
388
|
+
) as resp:
|
|
389
|
+
if resp.status >= 400:
|
|
390
|
+
body = await resp.text()
|
|
391
|
+
raise RoomException(
|
|
392
|
+
f"Failed to upload file. Status code: {resp.status}, body: {body}"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
async def download(self, *, project_id: str, path: str) -> bytes:
|
|
396
|
+
"""Download a file from project storage.
|
|
397
|
+
|
|
398
|
+
Corresponds to: **POST /projects/:project_id/storage/download** (HTTP GET in client)
|
|
399
|
+
Query params: `path`
|
|
400
|
+
Returns raw bytes of the file.
|
|
401
|
+
Raises NotFoundException for 404, RoomException for other HTTP errors.
|
|
402
|
+
"""
|
|
403
|
+
url = f"{self.base_url}/projects/{project_id}/storage/download"
|
|
404
|
+
params = {"path": path}
|
|
405
|
+
|
|
406
|
+
async with self._session.get(
|
|
407
|
+
url, params=params, headers=self._get_headers()
|
|
408
|
+
) as resp:
|
|
409
|
+
if resp.status == 404:
|
|
410
|
+
raise RoomException("file was not found")
|
|
411
|
+
if resp.status >= 400:
|
|
412
|
+
body = await resp.text()
|
|
413
|
+
raise RoomException(
|
|
414
|
+
f"Failed to download file. Status code: {resp.status}, body: {body}"
|
|
415
|
+
)
|
|
416
|
+
return await resp.read()
|
|
417
|
+
|
|
418
|
+
async def get_project_role(self, project_id: str) -> ProjectRole:
|
|
419
|
+
"""
|
|
420
|
+
Corresponds to: GET /accounts/projects/{id}/role
|
|
421
|
+
Returns a JSON dict with { "role" : "member" | "admin" } on success.
|
|
422
|
+
"""
|
|
423
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/role"
|
|
424
|
+
|
|
425
|
+
async with self._session.get(
|
|
426
|
+
url,
|
|
427
|
+
headers=self._get_headers(),
|
|
428
|
+
) as resp:
|
|
429
|
+
await self._raise_for_status(resp)
|
|
430
|
+
payload = await resp.json()
|
|
431
|
+
return cast(ProjectRole, payload["role"])
|
|
432
|
+
|
|
433
|
+
async def create_share(
|
|
434
|
+
self, project_id: str, settings: Optional[dict] = None
|
|
435
|
+
) -> Dict[str, Any]:
|
|
436
|
+
"""
|
|
437
|
+
Corresponds to: POST /accounts/projects
|
|
438
|
+
Body: { "name": "<name>" }
|
|
439
|
+
Returns a JSON dict with { "id" } on success.
|
|
440
|
+
"""
|
|
441
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/shares"
|
|
442
|
+
|
|
443
|
+
payload = {"settings": settings or {}}
|
|
444
|
+
|
|
445
|
+
async with self._session.post(
|
|
446
|
+
url,
|
|
447
|
+
headers=self._get_headers(),
|
|
448
|
+
json=payload,
|
|
449
|
+
) as resp:
|
|
450
|
+
await self._raise_for_status(resp)
|
|
451
|
+
return await resp.json()
|
|
452
|
+
|
|
453
|
+
async def delete_share(self, project_id: str, share_id: str) -> None:
|
|
454
|
+
"""
|
|
455
|
+
Corresponds to: DELETE /accounts/projects/:id/shares/:share_id
|
|
456
|
+
"""
|
|
457
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/shares/{share_id}"
|
|
458
|
+
|
|
459
|
+
async with self._session.delete(
|
|
460
|
+
url,
|
|
461
|
+
headers=self._get_headers(),
|
|
462
|
+
) as resp:
|
|
463
|
+
await self._raise_for_status(resp)
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
async def update_share(
|
|
467
|
+
self, project_id: str, share_id: str, settings: Optional[dict] = None
|
|
468
|
+
) -> None:
|
|
469
|
+
"""
|
|
470
|
+
Corresponds to: PUT /accounts/projects/:id/shares/:share_id
|
|
471
|
+
Body: { "settings" }
|
|
472
|
+
"""
|
|
473
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/shares/{share_id}"
|
|
474
|
+
|
|
475
|
+
payload = {"settings": settings or {}}
|
|
476
|
+
|
|
477
|
+
async with self._session.put(
|
|
478
|
+
url,
|
|
479
|
+
headers=self._get_headers(),
|
|
480
|
+
json=payload,
|
|
481
|
+
) as resp:
|
|
482
|
+
await self._raise_for_status(resp)
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
async def list_shares(self, project_id: str) -> list[RoomShare]:
|
|
486
|
+
"""
|
|
487
|
+
Corresponds to: GET /accounts/projects/:id/shares
|
|
488
|
+
Returns a JSON dict with { "shares" : [{ "id", "settings" }] } on success.
|
|
489
|
+
"""
|
|
490
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/shares"
|
|
491
|
+
|
|
492
|
+
async with self._session.get(
|
|
493
|
+
url,
|
|
494
|
+
headers=self._get_headers(),
|
|
495
|
+
) as resp:
|
|
496
|
+
await self._raise_for_status(resp)
|
|
497
|
+
data = await resp.json()
|
|
498
|
+
try:
|
|
499
|
+
return [RoomShare.model_validate(item) for item in data["shares"]]
|
|
500
|
+
except (KeyError, ValidationError) as exc:
|
|
501
|
+
raise RoomException(f"Invalid shares payload: {exc}") from exc
|
|
502
|
+
|
|
503
|
+
async def connect_share(self, share_id: str) -> RoomShareConnectionInfo:
|
|
504
|
+
"""
|
|
505
|
+
Corresponds to: POST /shares/:share_id/connect
|
|
506
|
+
Body: {}
|
|
507
|
+
Returns a JSON dict with { "jwt", "room_url" } on success.
|
|
508
|
+
"""
|
|
509
|
+
url = f"{self.base_url}/shares/{share_id}/connect"
|
|
510
|
+
|
|
511
|
+
async with self._session.post(
|
|
512
|
+
url,
|
|
513
|
+
headers=self._get_headers(),
|
|
514
|
+
json={},
|
|
515
|
+
) as resp:
|
|
516
|
+
await self._raise_for_status(resp)
|
|
517
|
+
data = await resp.json()
|
|
518
|
+
try:
|
|
519
|
+
return RoomShareConnectionInfo.model_validate(data)
|
|
520
|
+
except ValidationError as exc:
|
|
521
|
+
raise RoomException(f"Invalid share connection payload: {exc}") from exc
|
|
522
|
+
|
|
523
|
+
async def create_project(
|
|
524
|
+
self, name: str, settings: Optional[dict] = None
|
|
525
|
+
) -> Dict[str, Any]:
|
|
526
|
+
"""
|
|
527
|
+
Corresponds to: POST /accounts/projects
|
|
528
|
+
Body: { "name": "<name>" }
|
|
529
|
+
Returns a JSON dict with { "id", "owner_user_id", "name" } on success.
|
|
530
|
+
"""
|
|
531
|
+
url = f"{self.base_url}/accounts/projects"
|
|
532
|
+
|
|
533
|
+
async with self._session.post(
|
|
534
|
+
url,
|
|
535
|
+
headers=self._get_headers(),
|
|
536
|
+
json={"name": name, "settings": settings},
|
|
537
|
+
) as resp:
|
|
538
|
+
await self._raise_for_status(resp)
|
|
539
|
+
return await resp.json()
|
|
540
|
+
|
|
541
|
+
async def add_user_to_project(
|
|
542
|
+
self,
|
|
543
|
+
project_id: str,
|
|
544
|
+
user_id: str,
|
|
545
|
+
is_admin: bool = False,
|
|
546
|
+
is_developer: bool = False,
|
|
547
|
+
can_create_rooms: bool = False,
|
|
548
|
+
) -> Dict[str, Any]:
|
|
549
|
+
"""
|
|
550
|
+
Corresponds to: POST /accounts/projects/:id/users
|
|
551
|
+
Body: { "project_id", "user_id" }
|
|
552
|
+
Returns a JSON dict with { "ok": True } on success.
|
|
553
|
+
"""
|
|
554
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/users"
|
|
555
|
+
body = {
|
|
556
|
+
"project_id": project_id,
|
|
557
|
+
"user_id": user_id,
|
|
558
|
+
"is_admin": is_admin,
|
|
559
|
+
"is_developer": is_developer,
|
|
560
|
+
"can_create_rooms": can_create_rooms,
|
|
561
|
+
}
|
|
562
|
+
async with self._session.post(
|
|
563
|
+
url, headers=self._get_headers(), json=body
|
|
564
|
+
) as resp:
|
|
565
|
+
await self._raise_for_status(resp)
|
|
566
|
+
return await resp.json()
|
|
567
|
+
|
|
568
|
+
async def remove_user_from_project(
|
|
569
|
+
self, project_id: str, user_id: str
|
|
570
|
+
) -> Dict[str, Any]:
|
|
571
|
+
"""
|
|
572
|
+
Corresponds to: DELETE /accounts/projects/:project_id/users/:user_id
|
|
573
|
+
Returns a JSON dict with { "ok": True } on success.
|
|
574
|
+
"""
|
|
575
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/users/{user_id}"
|
|
576
|
+
|
|
577
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
578
|
+
await self._raise_for_status(resp)
|
|
579
|
+
return await resp.json()
|
|
580
|
+
|
|
581
|
+
async def update_project_settings(
|
|
582
|
+
self, project_id: str, settings: dict
|
|
583
|
+
) -> Dict[str, Any]:
|
|
584
|
+
"""
|
|
585
|
+
Corresponds to: PUT /accounts/projects/:id/settings
|
|
586
|
+
"""
|
|
587
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/settings"
|
|
588
|
+
|
|
589
|
+
async with self._session.put(
|
|
590
|
+
url, headers=self._get_headers(), json=settings
|
|
591
|
+
) as resp:
|
|
592
|
+
await self._raise_for_status(resp)
|
|
593
|
+
return await resp.json()
|
|
594
|
+
|
|
595
|
+
async def get_users_in_project(self, project_id: str) -> Dict[str, Any]:
|
|
596
|
+
"""
|
|
597
|
+
Corresponds to: GET /accounts/projects/:id/users
|
|
598
|
+
Returns a JSON dict with { "users": [...] }.
|
|
599
|
+
"""
|
|
600
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/users"
|
|
601
|
+
|
|
602
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
603
|
+
await self._raise_for_status(resp)
|
|
604
|
+
return await resp.json()
|
|
605
|
+
|
|
606
|
+
async def get_user_profile(self, user_id: str) -> Dict[str, Any]:
|
|
607
|
+
"""
|
|
608
|
+
Corresponds to: GET /accounts/profiles/:id
|
|
609
|
+
Returns the user profile JSON, e.g. { "id", "first_name", "last_name", "email" } or raises 404 if not found.
|
|
610
|
+
"""
|
|
611
|
+
url = f"{self.base_url}/accounts/profiles/{user_id}"
|
|
612
|
+
|
|
613
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
614
|
+
await self._raise_for_status(resp)
|
|
615
|
+
return await resp.json()
|
|
616
|
+
|
|
617
|
+
async def update_user_profile(
|
|
618
|
+
self, user_id: str, first_name: str, last_name: str
|
|
619
|
+
) -> Dict[str, Any]:
|
|
620
|
+
"""
|
|
621
|
+
Corresponds to: PUT /accounts/profiles/:id
|
|
622
|
+
Body: { "first_name", "last_name" }
|
|
623
|
+
Returns a JSON dict with { "ok": True } on success.
|
|
624
|
+
"""
|
|
625
|
+
url = f"{self.base_url}/accounts/profiles/{user_id}"
|
|
626
|
+
body = {"first_name": first_name, "last_name": last_name}
|
|
627
|
+
|
|
628
|
+
async with self._session.put(
|
|
629
|
+
url, headers=self._get_headers(), json=body
|
|
630
|
+
) as resp:
|
|
631
|
+
await self._raise_for_status(resp)
|
|
632
|
+
return await resp.json()
|
|
633
|
+
|
|
634
|
+
async def list_projects(self) -> Dict[str, Any]:
|
|
635
|
+
"""
|
|
636
|
+
Corresponds to: GET /accounts/projects
|
|
637
|
+
Returns a JSON dict with { "projects": [...] }.
|
|
638
|
+
"""
|
|
639
|
+
url = f"{self.base_url}/accounts/projects"
|
|
640
|
+
|
|
641
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
642
|
+
await self._raise_for_status(resp)
|
|
643
|
+
return await resp.json()
|
|
644
|
+
|
|
645
|
+
async def get_project(self, project_id: str) -> Dict[str, Any]:
|
|
646
|
+
"""
|
|
647
|
+
Corresponds to: GET /accounts/projects
|
|
648
|
+
Returns a JSON dict with { "projects": [...] }.
|
|
649
|
+
"""
|
|
650
|
+
url = f"{self.base_url}/accounts/projects/{project_id}"
|
|
651
|
+
|
|
652
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
653
|
+
await self._raise_for_status(resp)
|
|
654
|
+
return await resp.json()
|
|
655
|
+
|
|
656
|
+
async def create_api_key(
|
|
657
|
+
self, project_id: str, name: str, description: str
|
|
658
|
+
) -> Dict[str, Any]:
|
|
659
|
+
"""
|
|
660
|
+
Corresponds to: POST /accounts/projects/{project_id}/api-keys
|
|
661
|
+
Body: { "name": "<>", "description": "<>" }
|
|
662
|
+
Returns a JSON dict with { "id", "name", "description", "value" }.
|
|
663
|
+
"""
|
|
664
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/api-keys"
|
|
665
|
+
payload = {"name": name, "description": description}
|
|
666
|
+
|
|
667
|
+
async with self._session.post(
|
|
668
|
+
url, headers=self._get_headers(), json=payload
|
|
669
|
+
) as resp:
|
|
670
|
+
await self._raise_for_status(resp)
|
|
671
|
+
return await resp.json()
|
|
672
|
+
|
|
673
|
+
async def get_pricing(self) -> Dict[str, Any]:
|
|
674
|
+
"""GET /pricing"""
|
|
675
|
+
url = f"{self.base_url}/pricing"
|
|
676
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
677
|
+
await self._ensure_success(resp, action="fetch pricing data")
|
|
678
|
+
return await resp.json()
|
|
679
|
+
|
|
680
|
+
async def get_status(self, project_id: str) -> bool:
|
|
681
|
+
"""GET /accounts/projects/{project_id}/status"""
|
|
682
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/status"
|
|
683
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
684
|
+
await self._ensure_success(resp, action="fetch project status")
|
|
685
|
+
data = await resp.json()
|
|
686
|
+
enabled = data.get("enabled")
|
|
687
|
+
if not isinstance(enabled, bool):
|
|
688
|
+
raise RoomException(
|
|
689
|
+
"Invalid status payload: expected boolean 'enabled'"
|
|
690
|
+
)
|
|
691
|
+
return enabled
|
|
692
|
+
|
|
693
|
+
async def get_balance(self, project_id: str) -> Balance:
|
|
694
|
+
"""GET /accounts/projects/{project_id}/balance"""
|
|
695
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/balance"
|
|
696
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
697
|
+
await self._ensure_success(resp, action="fetch balance")
|
|
698
|
+
data = await resp.json()
|
|
699
|
+
try:
|
|
700
|
+
return Balance.model_validate(data)
|
|
701
|
+
except ValidationError as exc:
|
|
702
|
+
raise RoomException(f"Invalid balance payload: {exc}") from exc
|
|
703
|
+
|
|
704
|
+
async def get_recent_transactions(self, project_id: str) -> List[Transaction]:
|
|
705
|
+
"""GET /accounts/projects/{project_id}/transactions"""
|
|
706
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/transactions"
|
|
707
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
708
|
+
await self._ensure_success(resp, action="fetch transactions")
|
|
709
|
+
data = await resp.json()
|
|
710
|
+
transactions = data.get("transactions", [])
|
|
711
|
+
if not isinstance(transactions, list):
|
|
712
|
+
raise RoomException(
|
|
713
|
+
"Invalid transactions payload: expected 'transactions' list"
|
|
714
|
+
)
|
|
715
|
+
try:
|
|
716
|
+
return [Transaction.model_validate(item) for item in transactions]
|
|
717
|
+
except ValidationError as exc:
|
|
718
|
+
raise RoomException(f"Invalid transaction payload: {exc}") from exc
|
|
719
|
+
|
|
720
|
+
async def set_auto_recharge(
|
|
721
|
+
self,
|
|
722
|
+
*,
|
|
723
|
+
project_id: str,
|
|
724
|
+
enabled: bool,
|
|
725
|
+
amount: float,
|
|
726
|
+
threshold: float,
|
|
727
|
+
) -> None:
|
|
728
|
+
"""POST /accounts/projects/{project_id}/recharge"""
|
|
729
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/recharge"
|
|
730
|
+
payload = {
|
|
731
|
+
"enabled": enabled,
|
|
732
|
+
"amount": amount,
|
|
733
|
+
"threshold": threshold,
|
|
734
|
+
}
|
|
735
|
+
async with self._session.post(
|
|
736
|
+
url, headers=self._get_headers(), json=payload
|
|
737
|
+
) as resp:
|
|
738
|
+
await self._ensure_success(resp, action="update auto recharge settings")
|
|
739
|
+
|
|
740
|
+
async def get_checkout_url(
|
|
741
|
+
self,
|
|
742
|
+
project_id: str,
|
|
743
|
+
*,
|
|
744
|
+
success_url: str,
|
|
745
|
+
cancel_url: str,
|
|
746
|
+
) -> str:
|
|
747
|
+
"""POST /accounts/projects/{project_id}/subscription"""
|
|
748
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/subscription"
|
|
749
|
+
payload = {"success_url": success_url, "cancel_url": cancel_url}
|
|
750
|
+
async with self._session.post(
|
|
751
|
+
url, headers=self._get_headers(), json=payload
|
|
752
|
+
) as resp:
|
|
753
|
+
await self._ensure_success(resp, action="create subscription checkout")
|
|
754
|
+
data = await resp.json()
|
|
755
|
+
checkout_url = data.get("checkout_url")
|
|
756
|
+
if not isinstance(checkout_url, str):
|
|
757
|
+
raise RoomException(
|
|
758
|
+
"Invalid subscription payload: expected 'checkout_url' string"
|
|
759
|
+
)
|
|
760
|
+
return checkout_url
|
|
761
|
+
|
|
762
|
+
async def get_credits_checkout_url(
|
|
763
|
+
self,
|
|
764
|
+
project_id: str,
|
|
765
|
+
*,
|
|
766
|
+
success_url: str,
|
|
767
|
+
cancel_url: str,
|
|
768
|
+
quantity: float,
|
|
769
|
+
) -> str:
|
|
770
|
+
"""POST /accounts/projects/{project_id}/credits"""
|
|
771
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/credits"
|
|
772
|
+
payload = {
|
|
773
|
+
"quantity": quantity,
|
|
774
|
+
"success_url": success_url,
|
|
775
|
+
"cancel_url": cancel_url,
|
|
776
|
+
}
|
|
777
|
+
async with self._session.post(
|
|
778
|
+
url, headers=self._get_headers(), json=payload
|
|
779
|
+
) as resp:
|
|
780
|
+
await self._ensure_success(resp, action="create credits checkout")
|
|
781
|
+
data = await resp.json()
|
|
782
|
+
checkout_url = data.get("checkout_url")
|
|
783
|
+
if not isinstance(checkout_url, str):
|
|
784
|
+
raise RoomException(
|
|
785
|
+
"Invalid credits payload: expected 'checkout_url' string"
|
|
786
|
+
)
|
|
787
|
+
return checkout_url
|
|
788
|
+
|
|
789
|
+
async def get_subscription(self, project_id: str) -> Dict[str, Any]:
|
|
790
|
+
"""GET /accounts/projects/{project_id}/subscription"""
|
|
791
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/subscription"
|
|
792
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
793
|
+
await self._ensure_success(resp, action="fetch subscription")
|
|
794
|
+
return await resp.json()
|
|
795
|
+
|
|
796
|
+
async def get_usage(
|
|
797
|
+
self,
|
|
798
|
+
project_id: str,
|
|
799
|
+
*,
|
|
800
|
+
start: Optional[datetime] = None,
|
|
801
|
+
end: Optional[datetime] = None,
|
|
802
|
+
interval: Optional[str] = None,
|
|
803
|
+
report: Optional[str] = None,
|
|
804
|
+
) -> List[Dict[str, Any]]:
|
|
805
|
+
"""
|
|
806
|
+
Corresponds to: GET /accounts/projects/{project_id}/usage
|
|
807
|
+
Allows filtering using optional start/end timestamps, interval, and report name.
|
|
808
|
+
"""
|
|
809
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/usage"
|
|
810
|
+
params: Dict[str, str] = {}
|
|
811
|
+
if start is not None:
|
|
812
|
+
params["start"] = start.isoformat()
|
|
813
|
+
if end is not None:
|
|
814
|
+
params["end"] = end.isoformat()
|
|
815
|
+
if interval is not None:
|
|
816
|
+
params["interval"] = interval
|
|
817
|
+
if report is not None:
|
|
818
|
+
params["report"] = report
|
|
819
|
+
|
|
820
|
+
async with self._session.get(
|
|
821
|
+
url,
|
|
822
|
+
headers=self._get_headers(),
|
|
823
|
+
params=params or None,
|
|
824
|
+
) as resp:
|
|
825
|
+
await self._ensure_success(resp, action="retrieve usage")
|
|
826
|
+
data = await resp.json()
|
|
827
|
+
usage = data.get("usage", [])
|
|
828
|
+
if not isinstance(usage, list):
|
|
829
|
+
raise RoomException(
|
|
830
|
+
"Invalid usage payload: expected 'usage' to be a list"
|
|
831
|
+
)
|
|
832
|
+
return [item for item in usage if isinstance(item, dict)]
|
|
833
|
+
|
|
834
|
+
async def delete_api_key(self, project_id: str, id: str) -> None:
|
|
835
|
+
"""
|
|
836
|
+
Corresponds to: DELETE /accounts/projects/{project_id}/api-keys/{token_id}
|
|
837
|
+
Returns 204 No Content on success (no JSON body).
|
|
838
|
+
"""
|
|
839
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/api-keys/{id}"
|
|
840
|
+
|
|
841
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
842
|
+
await self._raise_for_status(resp)
|
|
843
|
+
# The server returns status 204 with no content, so no need to parse JSON.
|
|
844
|
+
|
|
845
|
+
async def list_api_keys(self, project_id: str) -> Dict[str, Any]:
|
|
846
|
+
"""
|
|
847
|
+
Corresponds to: GET /accounts/projects/{project_id}/api-keys
|
|
848
|
+
Returns a JSON dict like: { "tokens": [ { ... }, ... ] }.
|
|
849
|
+
"""
|
|
850
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/api-keys"
|
|
851
|
+
|
|
852
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
853
|
+
await self._raise_for_status(resp)
|
|
854
|
+
return await resp.json()
|
|
855
|
+
|
|
856
|
+
async def get_session(self, project_id: str, session_id: str) -> Dict[str, Any]:
|
|
857
|
+
"""
|
|
858
|
+
Corresponds to: GET /accounts/projects/{project_id}/sessions/{session_id}
|
|
859
|
+
Returns a JSON dict: { "id", "room_name", "created_at }
|
|
860
|
+
"""
|
|
861
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/sessions/{session_id}"
|
|
862
|
+
|
|
863
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
864
|
+
await self._raise_for_status(resp)
|
|
865
|
+
return await resp.json()
|
|
866
|
+
|
|
867
|
+
async def list_active_sessions(self, project_id: str) -> list[RoomSession]:
|
|
868
|
+
"""
|
|
869
|
+
Corresponds to: GET /accounts/projects/{project_id}/sessions
|
|
870
|
+
Returns a JSON dict: { "sessions": [...] }
|
|
871
|
+
"""
|
|
872
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/sessions/active"
|
|
873
|
+
|
|
874
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
875
|
+
await self._raise_for_status(resp)
|
|
876
|
+
data = await resp.json()
|
|
877
|
+
sessions = data.get("sessions", [])
|
|
878
|
+
return [RoomSession.model_validate(session) for session in sessions]
|
|
879
|
+
|
|
880
|
+
async def list_recent_sessions(self, project_id: str) -> list[RoomSession]:
|
|
881
|
+
"""
|
|
882
|
+
Corresponds to: GET /accounts/projects/{project_id}/sessions
|
|
883
|
+
Returns a JSON dict: { "sessions": [...] }
|
|
884
|
+
"""
|
|
885
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/sessions"
|
|
886
|
+
|
|
887
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
888
|
+
await self._raise_for_status(resp)
|
|
889
|
+
data = await resp.json()
|
|
890
|
+
sessions = data.get("sessions", [])
|
|
891
|
+
return [RoomSession.model_validate(session) for session in sessions]
|
|
892
|
+
|
|
893
|
+
async def list_session_events(
|
|
894
|
+
self, project_id: str, session_id: str
|
|
895
|
+
) -> list[Dict[str, Any]]:
|
|
896
|
+
"""
|
|
897
|
+
Corresponds to: GET /accounts/projects/{project_id}/sessions/{session_id}/events
|
|
898
|
+
Returns a JSON dict: { "events": [...] }
|
|
899
|
+
"""
|
|
900
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/sessions/{session_id}/events"
|
|
901
|
+
|
|
902
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
903
|
+
await self._raise_for_status(resp)
|
|
904
|
+
data = await resp.json()
|
|
905
|
+
return data.get("events", [])
|
|
906
|
+
|
|
907
|
+
async def terminate(self, project_id: str, session_id: str) -> None:
|
|
908
|
+
"""
|
|
909
|
+
Corresponds to: POST /accounts/projects/{project_id}/sessions/{session_id}/terminate
|
|
910
|
+
Returns 204 No Content on success.
|
|
911
|
+
"""
|
|
912
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/sessions/{session_id}/terminate"
|
|
913
|
+
|
|
914
|
+
async with self._session.post(url, headers=self._get_headers()) as resp:
|
|
915
|
+
await self._raise_for_status(resp)
|
|
916
|
+
|
|
917
|
+
async def list_session_spans(
|
|
918
|
+
self, project_id: str, session_id: str
|
|
919
|
+
) -> list[Dict[str, Any]]:
|
|
920
|
+
"""
|
|
921
|
+
Corresponds to: GET /accounts/projects/{project_id}/sessions/{session_id}/spans
|
|
922
|
+
Returns a JSON dict: { "spans": [...] }
|
|
923
|
+
"""
|
|
924
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/sessions/{session_id}/spans"
|
|
925
|
+
|
|
926
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
927
|
+
await self._raise_for_status(resp)
|
|
928
|
+
data = await resp.json()
|
|
929
|
+
return data.get("spans", [])
|
|
930
|
+
|
|
931
|
+
async def list_session_metrics(
|
|
932
|
+
self, project_id: str, session_id: str
|
|
933
|
+
) -> list[Dict[str, Any]]:
|
|
934
|
+
"""
|
|
935
|
+
Corresponds to: GET /accounts/projects/{project_id}/sessions/{session_id}/metrics
|
|
936
|
+
Returns a JSON dict: { "metrics": [...] }
|
|
937
|
+
"""
|
|
938
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/sessions/{session_id}/metrics"
|
|
939
|
+
|
|
940
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
941
|
+
await self._raise_for_status(resp)
|
|
942
|
+
data = await resp.json()
|
|
943
|
+
return data.get("metrics", [])
|
|
944
|
+
|
|
945
|
+
async def create_webhook(
|
|
946
|
+
self,
|
|
947
|
+
project_id: str,
|
|
948
|
+
name: str,
|
|
949
|
+
url: str,
|
|
950
|
+
events: List[str],
|
|
951
|
+
description: str = "",
|
|
952
|
+
action: Optional[str] = "",
|
|
953
|
+
payload: Optional[dict] = "",
|
|
954
|
+
) -> Dict[str, Any]:
|
|
955
|
+
"""
|
|
956
|
+
Corresponds to: POST /accounts/projects/{project_id}/webhooks
|
|
957
|
+
Body: { "name", "description", "url", "events" }
|
|
958
|
+
The server might generate an internal webhook_id (or retrieve it from the request).
|
|
959
|
+
Returns whatever JSON object the server responds with (likely empty or your new resource data).
|
|
960
|
+
"""
|
|
961
|
+
endpoint = f"{self.base_url}/accounts/projects/{project_id}/webhooks"
|
|
962
|
+
payload = {
|
|
963
|
+
"name": name,
|
|
964
|
+
"description": description,
|
|
965
|
+
"url": url,
|
|
966
|
+
"events": events,
|
|
967
|
+
"action": action,
|
|
968
|
+
"payload": payload,
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async with self._session.post(
|
|
972
|
+
endpoint, headers=self._get_headers(), json=payload
|
|
973
|
+
) as resp:
|
|
974
|
+
await self._raise_for_status(resp)
|
|
975
|
+
# If the server returns JSON with newly created webhook info, parse it:
|
|
976
|
+
return await resp.json()
|
|
977
|
+
|
|
978
|
+
async def update_webhook(
|
|
979
|
+
self,
|
|
980
|
+
project_id: str,
|
|
981
|
+
webhook_id: str,
|
|
982
|
+
name: str,
|
|
983
|
+
url: str,
|
|
984
|
+
events: List[str],
|
|
985
|
+
description: str = "",
|
|
986
|
+
action: Optional[str] = None,
|
|
987
|
+
payload: Optional[dict] = None,
|
|
988
|
+
) -> Dict[str, Any]:
|
|
989
|
+
"""
|
|
990
|
+
Corresponds to: PUT /accounts/projects/{project_id}/webhooks/{webhook_id}
|
|
991
|
+
Body: { "name", "description", "url", "events" }
|
|
992
|
+
Returns JSON (could be the updated resource or an empty dict).
|
|
993
|
+
"""
|
|
994
|
+
endpoint = (
|
|
995
|
+
f"{self.base_url}/accounts/projects/{project_id}/webhooks/{webhook_id}"
|
|
996
|
+
)
|
|
997
|
+
payload = {
|
|
998
|
+
"name": name,
|
|
999
|
+
"description": description,
|
|
1000
|
+
"url": url,
|
|
1001
|
+
"events": events,
|
|
1002
|
+
"action": action,
|
|
1003
|
+
"payload": payload,
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async with self._session.put(
|
|
1007
|
+
endpoint, headers=self._get_headers(), json=payload
|
|
1008
|
+
) as resp:
|
|
1009
|
+
await self._raise_for_status(resp)
|
|
1010
|
+
return await resp.json()
|
|
1011
|
+
|
|
1012
|
+
async def list_webhooks(self, project_id: str) -> Dict[str, Any]:
|
|
1013
|
+
"""
|
|
1014
|
+
Corresponds to: GET /accounts/projects/{project_id}/webhooks
|
|
1015
|
+
Returns a JSON dict like: { "webhooks": [ { ... }, ... ] }
|
|
1016
|
+
"""
|
|
1017
|
+
endpoint = f"{self.base_url}/accounts/projects/{project_id}/webhooks"
|
|
1018
|
+
|
|
1019
|
+
async with self._session.get(endpoint, headers=self._get_headers()) as resp:
|
|
1020
|
+
await self._raise_for_status(resp)
|
|
1021
|
+
return await resp.json()
|
|
1022
|
+
|
|
1023
|
+
async def delete_webhook(self, project_id: str, webhook_id: str) -> None:
|
|
1024
|
+
"""
|
|
1025
|
+
Corresponds to: DELETE /accounts/projects/{project_id}/webhooks/{webhook_id}
|
|
1026
|
+
Typically returns 200 or 204 on success (no JSON body).
|
|
1027
|
+
"""
|
|
1028
|
+
endpoint = (
|
|
1029
|
+
f"{self.base_url}/accounts/projects/{project_id}/webhooks/{webhook_id}"
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
async with self._session.delete(endpoint, headers=self._get_headers()) as resp:
|
|
1033
|
+
await self._raise_for_status(resp)
|
|
1034
|
+
|
|
1035
|
+
async def create_mailbox(
|
|
1036
|
+
self,
|
|
1037
|
+
*,
|
|
1038
|
+
project_id: str,
|
|
1039
|
+
address: str,
|
|
1040
|
+
room: str,
|
|
1041
|
+
queue: str,
|
|
1042
|
+
public: bool = False,
|
|
1043
|
+
) -> None:
|
|
1044
|
+
"""
|
|
1045
|
+
POST /accounts/projects/{project_id}/mailboxes
|
|
1046
|
+
Body: { "address", "room", "queue" }
|
|
1047
|
+
Returns {} on success.
|
|
1048
|
+
"""
|
|
1049
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/mailboxes"
|
|
1050
|
+
payload = _CreateMailboxRequest(
|
|
1051
|
+
address=address,
|
|
1052
|
+
room=room,
|
|
1053
|
+
queue=queue,
|
|
1054
|
+
public=public,
|
|
1055
|
+
).model_dump(mode="json")
|
|
1056
|
+
async with self._session.post(
|
|
1057
|
+
url, headers=self._get_headers(), json=payload
|
|
1058
|
+
) as resp:
|
|
1059
|
+
await self._raise_for_status(resp)
|
|
1060
|
+
|
|
1061
|
+
async def update_mailbox(
|
|
1062
|
+
self,
|
|
1063
|
+
*,
|
|
1064
|
+
project_id: str,
|
|
1065
|
+
address: str,
|
|
1066
|
+
room: str,
|
|
1067
|
+
queue: str,
|
|
1068
|
+
public: bool = False,
|
|
1069
|
+
) -> None:
|
|
1070
|
+
"""
|
|
1071
|
+
PUT /accounts/projects/{project_id}/mailboxes/{address}
|
|
1072
|
+
Body: { "room", "queue" }
|
|
1073
|
+
Returns {} on success.
|
|
1074
|
+
"""
|
|
1075
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/mailboxes/{address}"
|
|
1076
|
+
payload = _UpdateMailboxRequest(
|
|
1077
|
+
room=room, queue=queue, public=public
|
|
1078
|
+
).model_dump(mode="json")
|
|
1079
|
+
async with self._session.put(
|
|
1080
|
+
url, headers=self._get_headers(), json=payload
|
|
1081
|
+
) as resp:
|
|
1082
|
+
await self._raise_for_status(resp)
|
|
1083
|
+
|
|
1084
|
+
async def get_mailbox(self, *, project_id: str, address: str) -> Mailbox:
|
|
1085
|
+
"""
|
|
1086
|
+
GET /accounts/projects/{project_id}/mailboxes/{address}
|
|
1087
|
+
Returns a list[Mailbox].
|
|
1088
|
+
"""
|
|
1089
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/mailboxes/{address}"
|
|
1090
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1091
|
+
await self._raise_for_status(resp)
|
|
1092
|
+
return Mailbox.model_validate((await resp.json())["mailbox"])
|
|
1093
|
+
|
|
1094
|
+
async def list_mailboxes(self, *, project_id: str) -> List[Mailbox]:
|
|
1095
|
+
"""
|
|
1096
|
+
GET /accounts/projects/{project_id}/mailboxes
|
|
1097
|
+
Returns a list[Mailbox].
|
|
1098
|
+
"""
|
|
1099
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/mailboxes"
|
|
1100
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1101
|
+
await self._raise_for_status(resp)
|
|
1102
|
+
data = await resp.json()
|
|
1103
|
+
try:
|
|
1104
|
+
return [Mailbox.model_validate(item) for item in data["mailboxes"]]
|
|
1105
|
+
except ValidationError as exc:
|
|
1106
|
+
raise RoomException(f"Invalid mailboxes payload: {exc}") from exc
|
|
1107
|
+
|
|
1108
|
+
async def delete_mailbox(self, *, project_id: str, address: str) -> None:
|
|
1109
|
+
"""
|
|
1110
|
+
DELETE /accounts/projects/{project_id}/mailboxes/{address}
|
|
1111
|
+
Returns {} on success.
|
|
1112
|
+
"""
|
|
1113
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/mailboxes/{address}"
|
|
1114
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1115
|
+
await self._raise_for_status(resp)
|
|
1116
|
+
|
|
1117
|
+
async def create_service(
|
|
1118
|
+
self,
|
|
1119
|
+
*,
|
|
1120
|
+
project_id: str,
|
|
1121
|
+
service: ServiceSpec,
|
|
1122
|
+
) -> str:
|
|
1123
|
+
"""
|
|
1124
|
+
POST /accounts/projects/{project_id}/services
|
|
1125
|
+
Body: full service spec, e.g.
|
|
1126
|
+
{
|
|
1127
|
+
"name": "...",
|
|
1128
|
+
"image": "...",
|
|
1129
|
+
"pull_secret": "...",
|
|
1130
|
+
"environment": {...},
|
|
1131
|
+
"environment_secrets": [...],
|
|
1132
|
+
"runtime_secrets": {...},
|
|
1133
|
+
"command": "...",
|
|
1134
|
+
"ports": {...}
|
|
1135
|
+
}
|
|
1136
|
+
Returns: { "id": "<service_id>" }
|
|
1137
|
+
"""
|
|
1138
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/services"
|
|
1139
|
+
async with self._session.post(
|
|
1140
|
+
url,
|
|
1141
|
+
headers=self._get_headers(),
|
|
1142
|
+
json=service.model_dump(mode="json", exclude_unset=True),
|
|
1143
|
+
) as resp:
|
|
1144
|
+
await self._raise_for_status(resp)
|
|
1145
|
+
return (await resp.json())["id"]
|
|
1146
|
+
|
|
1147
|
+
async def update_service(
|
|
1148
|
+
self,
|
|
1149
|
+
*,
|
|
1150
|
+
project_id: str,
|
|
1151
|
+
service_id: str,
|
|
1152
|
+
service: ServiceSpec,
|
|
1153
|
+
) -> None:
|
|
1154
|
+
"""
|
|
1155
|
+
PUT /accounts/projects/{project_id}/services/{service_id}
|
|
1156
|
+
Body: same structure as create_service (fields you wish to change).
|
|
1157
|
+
Returns: {} on success.
|
|
1158
|
+
"""
|
|
1159
|
+
|
|
1160
|
+
if service.id is None:
|
|
1161
|
+
raise RoomException("Service id must be set")
|
|
1162
|
+
|
|
1163
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/services/{service_id}"
|
|
1164
|
+
async with self._session.put(
|
|
1165
|
+
url, headers=self._get_headers(), json=service.model_dump(mode="json")
|
|
1166
|
+
) as resp:
|
|
1167
|
+
await self._raise_for_status(resp)
|
|
1168
|
+
await resp.json()
|
|
1169
|
+
|
|
1170
|
+
async def get_service(self, *, project_id: str, service_id: str) -> ServiceSpec:
|
|
1171
|
+
"""
|
|
1172
|
+
GET /accounts/projects/{project_id}/services/{service_id}
|
|
1173
|
+
Returns a `Service` instance.
|
|
1174
|
+
"""
|
|
1175
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/services/{service_id}"
|
|
1176
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1177
|
+
await self._raise_for_status(resp)
|
|
1178
|
+
# Handler returns a JSON string, so we read text then validate
|
|
1179
|
+
raw = await resp.text()
|
|
1180
|
+
try:
|
|
1181
|
+
return ServiceSpec.model_validate_json(raw)
|
|
1182
|
+
except ValidationError as exc:
|
|
1183
|
+
raise RoomException(f"Invalid service payload: {exc}") from exc
|
|
1184
|
+
|
|
1185
|
+
async def list_services(self, *, project_id: str) -> List[ServiceSpec]:
|
|
1186
|
+
"""
|
|
1187
|
+
GET /accounts/projects/{project_id}/services
|
|
1188
|
+
Returns a list of `Service` instances.
|
|
1189
|
+
"""
|
|
1190
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/services"
|
|
1191
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1192
|
+
await self._raise_for_status(resp)
|
|
1193
|
+
data = await resp.json()
|
|
1194
|
+
try:
|
|
1195
|
+
return [ServiceSpec.model_validate(item) for item in data["services"]]
|
|
1196
|
+
except ValidationError as exc:
|
|
1197
|
+
raise RoomException(f"Invalid services payload: {exc}") from exc
|
|
1198
|
+
|
|
1199
|
+
async def delete_service(self, *, project_id: str, service_id: str) -> None:
|
|
1200
|
+
"""
|
|
1201
|
+
DELETE /accounts/projects/{project_id}/services/{service_id}
|
|
1202
|
+
Returns nothing on success.
|
|
1203
|
+
"""
|
|
1204
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/services/{service_id}"
|
|
1205
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1206
|
+
await self._raise_for_status(resp)
|
|
1207
|
+
|
|
1208
|
+
async def create_room_service(
|
|
1209
|
+
self,
|
|
1210
|
+
*,
|
|
1211
|
+
project_id: str,
|
|
1212
|
+
room_name: str,
|
|
1213
|
+
service: ServiceSpec,
|
|
1214
|
+
) -> str:
|
|
1215
|
+
"""
|
|
1216
|
+
POST /accounts/projects/{project_id}/services
|
|
1217
|
+
Body: full service spec, e.g.
|
|
1218
|
+
{
|
|
1219
|
+
"name": "...",
|
|
1220
|
+
"image": "...",
|
|
1221
|
+
"pull_secret": "...",
|
|
1222
|
+
"environment": {...},
|
|
1223
|
+
"environment_secrets": [...],
|
|
1224
|
+
"runtime_secrets": {...},
|
|
1225
|
+
"command": "...",
|
|
1226
|
+
"ports": {...}
|
|
1227
|
+
}
|
|
1228
|
+
Returns: { "id": "<service_id>" }
|
|
1229
|
+
"""
|
|
1230
|
+
url = (
|
|
1231
|
+
f"{self.base_url}/accounts/projects/{project_id}/rooms/{room_name}/services"
|
|
1232
|
+
)
|
|
1233
|
+
async with self._session.post(
|
|
1234
|
+
url,
|
|
1235
|
+
headers=self._get_headers(),
|
|
1236
|
+
json=service.model_dump(mode="json", exclude_unset=True),
|
|
1237
|
+
) as resp:
|
|
1238
|
+
await self._raise_for_status(resp)
|
|
1239
|
+
return (await resp.json())["id"]
|
|
1240
|
+
|
|
1241
|
+
async def update_room_service(
|
|
1242
|
+
self,
|
|
1243
|
+
*,
|
|
1244
|
+
project_id: str,
|
|
1245
|
+
room_name: str,
|
|
1246
|
+
service_id: str,
|
|
1247
|
+
service: ServiceSpec,
|
|
1248
|
+
) -> NotImplemented:
|
|
1249
|
+
"""
|
|
1250
|
+
PUT /accounts/projects/{project_id}/services/{service_id}
|
|
1251
|
+
Body: same structure as create_service (fields you wish to change).
|
|
1252
|
+
Returns: {} on success.
|
|
1253
|
+
"""
|
|
1254
|
+
|
|
1255
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms/{room_name}/services/{service_id}"
|
|
1256
|
+
async with self._session.put(
|
|
1257
|
+
url, headers=self._get_headers(), json=service.model_dump(mode="json")
|
|
1258
|
+
) as resp:
|
|
1259
|
+
await self._raise_for_status(resp)
|
|
1260
|
+
await resp.json()
|
|
1261
|
+
|
|
1262
|
+
async def get_room_service(
|
|
1263
|
+
self, *, project_id: str, room_name: str, service_id: str
|
|
1264
|
+
) -> ServiceSpec:
|
|
1265
|
+
"""
|
|
1266
|
+
GET /accounts/projects/{project_id}/services/{service_id}
|
|
1267
|
+
Returns a `Service` instance.
|
|
1268
|
+
"""
|
|
1269
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms/{room_name}/services/{service_id}"
|
|
1270
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1271
|
+
await self._raise_for_status(resp)
|
|
1272
|
+
# Handler returns a JSON string, so we read text then validate
|
|
1273
|
+
raw = await resp.text()
|
|
1274
|
+
try:
|
|
1275
|
+
return ServiceSpec.model_validate_json(raw)
|
|
1276
|
+
except ValidationError as exc:
|
|
1277
|
+
raise RoomException(f"Invalid service payload: {exc}") from exc
|
|
1278
|
+
|
|
1279
|
+
async def list_room_services(
|
|
1280
|
+
self, *, project_id: str, room_name: str
|
|
1281
|
+
) -> List[ServiceSpec]:
|
|
1282
|
+
"""
|
|
1283
|
+
GET /accounts/projects/{project_id}/services
|
|
1284
|
+
Returns a list of `Service` instances.
|
|
1285
|
+
"""
|
|
1286
|
+
url = (
|
|
1287
|
+
f"{self.base_url}/accounts/projects/{project_id}/rooms/{room_name}/services"
|
|
1288
|
+
)
|
|
1289
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1290
|
+
await self._raise_for_status(resp)
|
|
1291
|
+
data = await resp.json()
|
|
1292
|
+
try:
|
|
1293
|
+
return [ServiceSpec.model_validate(item) for item in data["services"]]
|
|
1294
|
+
except ValidationError as exc:
|
|
1295
|
+
raise RoomException(f"Invalid services payload: {exc}") from exc
|
|
1296
|
+
|
|
1297
|
+
async def delete_room_service(
|
|
1298
|
+
self, *, project_id: str, room_name: str, service_id: str
|
|
1299
|
+
) -> None:
|
|
1300
|
+
"""
|
|
1301
|
+
DELETE /accounts/projects/{project_id}/services/{service_id}
|
|
1302
|
+
Returns nothing on success.
|
|
1303
|
+
"""
|
|
1304
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms/{room_name}/services/{service_id}"
|
|
1305
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1306
|
+
await self._raise_for_status(resp)
|
|
1307
|
+
|
|
1308
|
+
async def create_secret(
|
|
1309
|
+
self,
|
|
1310
|
+
*,
|
|
1311
|
+
project_id: str,
|
|
1312
|
+
secret: SecretLike,
|
|
1313
|
+
) -> str:
|
|
1314
|
+
"""
|
|
1315
|
+
POST /accounts/projects/{project_id}/secrets
|
|
1316
|
+
Returns the new secret_id.
|
|
1317
|
+
"""
|
|
1318
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/secrets"
|
|
1319
|
+
payload = {
|
|
1320
|
+
"name": secret.name,
|
|
1321
|
+
"type": secret.type, # "docker" | "keys"
|
|
1322
|
+
"data": secret.to_payload(), # already shaped for the provisioner
|
|
1323
|
+
}
|
|
1324
|
+
async with self._session.post(
|
|
1325
|
+
url, headers=self._get_headers(), json=payload
|
|
1326
|
+
) as resp:
|
|
1327
|
+
await self._raise_for_status(resp)
|
|
1328
|
+
return (await resp.json())["id"]
|
|
1329
|
+
|
|
1330
|
+
async def update_secret(
|
|
1331
|
+
self,
|
|
1332
|
+
*,
|
|
1333
|
+
project_id: str,
|
|
1334
|
+
secret: SecretLike,
|
|
1335
|
+
) -> None:
|
|
1336
|
+
"""
|
|
1337
|
+
PUT /accounts/projects/{project_id}/secrets/{secret.id}
|
|
1338
|
+
Body ➜ { "name", "type", "data" }
|
|
1339
|
+
"""
|
|
1340
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/secrets/{secret.id}"
|
|
1341
|
+
payload = {
|
|
1342
|
+
"name": secret.name,
|
|
1343
|
+
"type": secret.type,
|
|
1344
|
+
"data": secret.to_payload(),
|
|
1345
|
+
}
|
|
1346
|
+
async with self._session.put(
|
|
1347
|
+
url, headers=self._get_headers(), json=payload
|
|
1348
|
+
) as resp:
|
|
1349
|
+
await self._raise_for_status(resp)
|
|
1350
|
+
|
|
1351
|
+
async def delete_secret(self, *, project_id: str, secret_id: str) -> None:
|
|
1352
|
+
"""
|
|
1353
|
+
DELETE /accounts/projects/{project_id}/secrets/{secret_id}
|
|
1354
|
+
Returns {} (or 204 No Content) on success.
|
|
1355
|
+
"""
|
|
1356
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/secrets/{secret_id}"
|
|
1357
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1358
|
+
await self._raise_for_status(resp)
|
|
1359
|
+
|
|
1360
|
+
async def list_secrets(self, project_id: str) -> List[SecretLike]:
|
|
1361
|
+
"""
|
|
1362
|
+
GET /accounts/projects/{project_id}/secrets
|
|
1363
|
+
Returns [PullSecret | KeysSecret, …]
|
|
1364
|
+
"""
|
|
1365
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/secrets"
|
|
1366
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1367
|
+
await self._raise_for_status(resp)
|
|
1368
|
+
raw = await resp.json()
|
|
1369
|
+
return [_parse_secret(item) for item in raw["secrets"]]
|
|
1370
|
+
|
|
1371
|
+
async def create_room(
|
|
1372
|
+
self,
|
|
1373
|
+
*,
|
|
1374
|
+
project_id: str,
|
|
1375
|
+
name: str,
|
|
1376
|
+
if_not_exists: bool = False,
|
|
1377
|
+
metadata: Optional[dict[str, any]] = None,
|
|
1378
|
+
permissions: Optional[dict[str, ApiScope]] = None,
|
|
1379
|
+
) -> Room:
|
|
1380
|
+
"""
|
|
1381
|
+
POST /accounts/projects/{project_id}/rooms
|
|
1382
|
+
Body: { "name": str, "if_not_exists?": bool }
|
|
1383
|
+
Returns Room.
|
|
1384
|
+
"""
|
|
1385
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms"
|
|
1386
|
+
payload = {
|
|
1387
|
+
"name": name,
|
|
1388
|
+
"if_not_exists": bool(if_not_exists),
|
|
1389
|
+
"metadata": metadata,
|
|
1390
|
+
"permissions": permissions,
|
|
1391
|
+
}
|
|
1392
|
+
async with self._session.post(
|
|
1393
|
+
url, headers=self._get_headers(), json=payload
|
|
1394
|
+
) as resp:
|
|
1395
|
+
await self._raise_for_status(resp)
|
|
1396
|
+
try:
|
|
1397
|
+
return Room.model_validate(await resp.json())
|
|
1398
|
+
except ValidationError as exc:
|
|
1399
|
+
raise RoomException(f"Invalid room payload: {exc}") from exc
|
|
1400
|
+
|
|
1401
|
+
async def get_room(self, *, project_id: str, name: str) -> Room:
|
|
1402
|
+
"""
|
|
1403
|
+
GET /accounts/projects/{project_id}/rooms/{room_name}
|
|
1404
|
+
Returns Room.
|
|
1405
|
+
"""
|
|
1406
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms/{name}"
|
|
1407
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1408
|
+
if resp.status == 404:
|
|
1409
|
+
raise RoomException("room not found")
|
|
1410
|
+
await self._raise_for_status(resp)
|
|
1411
|
+
try:
|
|
1412
|
+
return Room.model_validate(await resp.json())
|
|
1413
|
+
except ValidationError as exc:
|
|
1414
|
+
raise RoomException(f"Invalid room payload: {exc}") from exc
|
|
1415
|
+
|
|
1416
|
+
async def update_room(
|
|
1417
|
+
self,
|
|
1418
|
+
*,
|
|
1419
|
+
project_id: str,
|
|
1420
|
+
room_id: str,
|
|
1421
|
+
name: str,
|
|
1422
|
+
metadata: Optional[dict[str, any]] = None,
|
|
1423
|
+
) -> None:
|
|
1424
|
+
"""
|
|
1425
|
+
PUT /accounts/projects/{project_id}/rooms/{room_id}
|
|
1426
|
+
Body: { "name": str }
|
|
1427
|
+
"""
|
|
1428
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms/{room_id}"
|
|
1429
|
+
payload = {"name": name}
|
|
1430
|
+
|
|
1431
|
+
if metadata is not None:
|
|
1432
|
+
payload["metadata"] = metadata
|
|
1433
|
+
|
|
1434
|
+
async with self._session.put(
|
|
1435
|
+
url, headers=self._get_headers(), json=payload
|
|
1436
|
+
) as resp:
|
|
1437
|
+
await self._raise_for_status(resp)
|
|
1438
|
+
|
|
1439
|
+
async def delete_room(self, *, project_id: str, room_id: str) -> None:
|
|
1440
|
+
"""
|
|
1441
|
+
DELETE /accounts/projects/{project_id}/rooms/{room_id}
|
|
1442
|
+
"""
|
|
1443
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms/{room_id}"
|
|
1444
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1445
|
+
await self._raise_for_status(resp)
|
|
1446
|
+
|
|
1447
|
+
async def connect_room(self, *, project_id: str, room: str) -> RoomConnectionInfo:
|
|
1448
|
+
"""
|
|
1449
|
+
POST /accounts/projects/{project_id}/rooms/{room_name}/connect
|
|
1450
|
+
Returns: { "jwt", "room_name", "project_id", "room_url" }
|
|
1451
|
+
"""
|
|
1452
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms/{room}/connect"
|
|
1453
|
+
async with self._session.post(
|
|
1454
|
+
url, headers=self._get_headers(), json={}
|
|
1455
|
+
) as resp:
|
|
1456
|
+
await self._raise_for_status(resp)
|
|
1457
|
+
return RoomConnectionInfo.model_validate(await resp.json())
|
|
1458
|
+
|
|
1459
|
+
async def create_room_grant(
|
|
1460
|
+
self,
|
|
1461
|
+
*,
|
|
1462
|
+
project_id: str,
|
|
1463
|
+
room_id: str,
|
|
1464
|
+
user_id: str,
|
|
1465
|
+
permissions: Dict[str, Any],
|
|
1466
|
+
) -> None:
|
|
1467
|
+
"""
|
|
1468
|
+
POST /accounts/projects/{project_id}/room-grants
|
|
1469
|
+
Body: { "room_id", "user_id", "permissions" }
|
|
1470
|
+
Returns {} on success.
|
|
1471
|
+
"""
|
|
1472
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/room-grants"
|
|
1473
|
+
payload = _CreateRoomGrantRequest(
|
|
1474
|
+
room_id=room_id,
|
|
1475
|
+
user_id=user_id,
|
|
1476
|
+
permissions=permissions,
|
|
1477
|
+
).model_dump(mode="json")
|
|
1478
|
+
|
|
1479
|
+
async with self._session.post(
|
|
1480
|
+
url, headers=self._get_headers(), json=payload
|
|
1481
|
+
) as resp:
|
|
1482
|
+
await self._raise_for_status(resp)
|
|
1483
|
+
|
|
1484
|
+
async def create_room_grant_by_email(
|
|
1485
|
+
self,
|
|
1486
|
+
*,
|
|
1487
|
+
project_id: str,
|
|
1488
|
+
room_id: str,
|
|
1489
|
+
email: str,
|
|
1490
|
+
permissions: ApiScope,
|
|
1491
|
+
) -> None:
|
|
1492
|
+
"""
|
|
1493
|
+
POST /accounts/projects/{project_id}/room-grants
|
|
1494
|
+
Body: { "room_id", "user_id", "permissions" }
|
|
1495
|
+
Returns {} on success.
|
|
1496
|
+
"""
|
|
1497
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/room-grants"
|
|
1498
|
+
payload = _CreateRoomGrantRequest(
|
|
1499
|
+
room_id=room_id,
|
|
1500
|
+
email=email,
|
|
1501
|
+
permissions=permissions,
|
|
1502
|
+
).model_dump(mode="json")
|
|
1503
|
+
|
|
1504
|
+
async with self._session.post(
|
|
1505
|
+
url, headers=self._get_headers(), json=payload
|
|
1506
|
+
) as resp:
|
|
1507
|
+
await self._raise_for_status(resp)
|
|
1508
|
+
|
|
1509
|
+
async def update_room_grant(
|
|
1510
|
+
self,
|
|
1511
|
+
*,
|
|
1512
|
+
project_id: str,
|
|
1513
|
+
room_id: str,
|
|
1514
|
+
user_id: str,
|
|
1515
|
+
permissions: ApiScope,
|
|
1516
|
+
grant_id: Optional[str] = None,
|
|
1517
|
+
) -> None:
|
|
1518
|
+
"""
|
|
1519
|
+
PUT /accounts/projects/{project_id}/room-grants/{grant_id}
|
|
1520
|
+
Body: { "room_id", "user_id", "permissions" }
|
|
1521
|
+
NOTE: The server handler currently ignores grant_id and updates by (project_id, room_id, user_id).
|
|
1522
|
+
"""
|
|
1523
|
+
gid = grant_id or "unused"
|
|
1524
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/room-grants/{gid}"
|
|
1525
|
+
payload = _UpdateRoomGrantRequest(
|
|
1526
|
+
room_id=room_id,
|
|
1527
|
+
user_id=user_id,
|
|
1528
|
+
permissions=permissions,
|
|
1529
|
+
).model_dump(mode="json")
|
|
1530
|
+
|
|
1531
|
+
async with self._session.put(
|
|
1532
|
+
url, headers=self._get_headers(), json=payload
|
|
1533
|
+
) as resp:
|
|
1534
|
+
await self._raise_for_status(resp)
|
|
1535
|
+
|
|
1536
|
+
async def delete_room_grant(
|
|
1537
|
+
self, *, project_id: str, room_id: str, user_id: str
|
|
1538
|
+
) -> None:
|
|
1539
|
+
"""
|
|
1540
|
+
DELETE /accounts/projects/{project_id}/room-grants/{room_id}/{user_id}
|
|
1541
|
+
Returns {} on success.
|
|
1542
|
+
"""
|
|
1543
|
+
from urllib.parse import quote
|
|
1544
|
+
|
|
1545
|
+
url = (
|
|
1546
|
+
f"{self.base_url}/accounts/projects/{project_id}"
|
|
1547
|
+
f"/room-grants/{quote(room_id, safe='')}/{quote(user_id, safe='')}"
|
|
1548
|
+
)
|
|
1549
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1550
|
+
await self._raise_for_status(resp)
|
|
1551
|
+
|
|
1552
|
+
async def get_room_grant(
|
|
1553
|
+
self, *, project_id: str, room_id: str, user_id: str
|
|
1554
|
+
) -> ProjectRoomGrant:
|
|
1555
|
+
"""
|
|
1556
|
+
GET /accounts/projects/{project_id}/room-grants/{room_id}/{user_id}
|
|
1557
|
+
Returns ProjectRoomGrant
|
|
1558
|
+
"""
|
|
1559
|
+
from urllib.parse import quote
|
|
1560
|
+
|
|
1561
|
+
url = (
|
|
1562
|
+
f"{self.base_url}/accounts/projects/{project_id}"
|
|
1563
|
+
f"/room-grants/{quote(room_id, safe='')}/{quote(user_id, safe='')}"
|
|
1564
|
+
)
|
|
1565
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1566
|
+
await self._raise_for_status(resp)
|
|
1567
|
+
data = await resp.json()
|
|
1568
|
+
try:
|
|
1569
|
+
return ProjectRoomGrant.model_validate(data)
|
|
1570
|
+
except ValidationError as exc:
|
|
1571
|
+
raise RoomException(f"Invalid room grant payload: {exc}") from exc
|
|
1572
|
+
|
|
1573
|
+
async def list_rooms(
|
|
1574
|
+
self,
|
|
1575
|
+
*,
|
|
1576
|
+
project_id: str,
|
|
1577
|
+
limit: int = 50,
|
|
1578
|
+
offset: int = 0,
|
|
1579
|
+
order_by: str = "room_name",
|
|
1580
|
+
) -> List[ProjectRoomGrant]:
|
|
1581
|
+
"""
|
|
1582
|
+
GET /accounts/projects/{project_id}/rooms?limit=&offset=&order_by=
|
|
1583
|
+
Returns [Rooms]
|
|
1584
|
+
"""
|
|
1585
|
+
params = {"limit": str(limit), "offset": str(offset), "order_by": order_by}
|
|
1586
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/rooms"
|
|
1587
|
+
async with self._session.get(
|
|
1588
|
+
url, headers=self._get_headers(), params=params
|
|
1589
|
+
) as resp:
|
|
1590
|
+
await self._raise_for_status(resp)
|
|
1591
|
+
data = await resp.json()
|
|
1592
|
+
try:
|
|
1593
|
+
return [Room.model_validate(item) for item in data["rooms"]]
|
|
1594
|
+
except ValidationError as exc:
|
|
1595
|
+
raise RoomException(f"Invalid rooms list payload: {exc}") from exc
|
|
1596
|
+
|
|
1597
|
+
async def list_room_grants(
|
|
1598
|
+
self,
|
|
1599
|
+
*,
|
|
1600
|
+
project_id: str,
|
|
1601
|
+
limit: int = 50,
|
|
1602
|
+
offset: int = 0,
|
|
1603
|
+
order_by: str = "room_name",
|
|
1604
|
+
) -> List[ProjectRoomGrant]:
|
|
1605
|
+
"""
|
|
1606
|
+
GET /accounts/projects/{project_id}/room-grants?limit=&offset=&order_by=
|
|
1607
|
+
Returns [ProjectRoomGrant]
|
|
1608
|
+
"""
|
|
1609
|
+
params = {"limit": str(limit), "offset": str(offset), "order_by": order_by}
|
|
1610
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/room-grants"
|
|
1611
|
+
async with self._session.get(
|
|
1612
|
+
url, headers=self._get_headers(), params=params
|
|
1613
|
+
) as resp:
|
|
1614
|
+
await self._raise_for_status(resp)
|
|
1615
|
+
data = await resp.json()
|
|
1616
|
+
try:
|
|
1617
|
+
return [
|
|
1618
|
+
ProjectRoomGrant.model_validate(item)
|
|
1619
|
+
for item in data["room_grants"]
|
|
1620
|
+
]
|
|
1621
|
+
except ValidationError as exc:
|
|
1622
|
+
raise RoomException(f"Invalid room grants list payload: {exc}") from exc
|
|
1623
|
+
|
|
1624
|
+
async def list_room_grants_by_user(
|
|
1625
|
+
self,
|
|
1626
|
+
*,
|
|
1627
|
+
project_id: str,
|
|
1628
|
+
user_id: str,
|
|
1629
|
+
limit: int = 50,
|
|
1630
|
+
offset: int = 0,
|
|
1631
|
+
) -> List[ProjectRoomGrant]:
|
|
1632
|
+
"""
|
|
1633
|
+
GET /accounts/projects/{project_id}/room-grants/by-user/{user_id}?limit=&offset=&order_by=
|
|
1634
|
+
Returns [ProjectRoomGrant]
|
|
1635
|
+
"""
|
|
1636
|
+
from urllib.parse import quote
|
|
1637
|
+
|
|
1638
|
+
params = {"limit": str(limit), "offset": str(offset)}
|
|
1639
|
+
url = (
|
|
1640
|
+
f"{self.base_url}/accounts/projects/{project_id}"
|
|
1641
|
+
f"/room-grants/by-user/{quote(user_id, safe='')}"
|
|
1642
|
+
)
|
|
1643
|
+
async with self._session.get(
|
|
1644
|
+
url, headers=self._get_headers(), params=params
|
|
1645
|
+
) as resp:
|
|
1646
|
+
await self._raise_for_status(resp)
|
|
1647
|
+
data = await resp.json()
|
|
1648
|
+
try:
|
|
1649
|
+
return [
|
|
1650
|
+
ProjectRoomGrant.model_validate(item)
|
|
1651
|
+
for item in data["room_grants"]
|
|
1652
|
+
]
|
|
1653
|
+
except ValidationError as exc:
|
|
1654
|
+
raise RoomException(
|
|
1655
|
+
f"Invalid room grants-by-user payload: {exc}"
|
|
1656
|
+
) from exc
|
|
1657
|
+
|
|
1658
|
+
async def list_room_grants_by_room(
|
|
1659
|
+
self,
|
|
1660
|
+
*,
|
|
1661
|
+
project_id: str,
|
|
1662
|
+
room_name: str,
|
|
1663
|
+
limit: int = 50,
|
|
1664
|
+
offset: int = 0,
|
|
1665
|
+
) -> List[ProjectRoomGrant]:
|
|
1666
|
+
"""
|
|
1667
|
+
GET /accounts/projects/{project_id}/room-grants/by-room/{room_id}?limit=&offset=
|
|
1668
|
+
Returns [ProjectRoomGrant]
|
|
1669
|
+
"""
|
|
1670
|
+
from urllib.parse import quote
|
|
1671
|
+
|
|
1672
|
+
params = {"limit": str(limit), "offset": str(offset)}
|
|
1673
|
+
url = (
|
|
1674
|
+
f"{self.base_url}/accounts/projects/{project_id}"
|
|
1675
|
+
f"/room-grants/by-room/{quote(room_name, safe='')}"
|
|
1676
|
+
)
|
|
1677
|
+
async with self._session.get(
|
|
1678
|
+
url, headers=self._get_headers(), params=params
|
|
1679
|
+
) as resp:
|
|
1680
|
+
await self._raise_for_status(resp)
|
|
1681
|
+
data = await resp.json()
|
|
1682
|
+
try:
|
|
1683
|
+
return [
|
|
1684
|
+
ProjectRoomGrant.model_validate(item)
|
|
1685
|
+
for item in data["room_grants"]
|
|
1686
|
+
]
|
|
1687
|
+
except ValidationError as exc:
|
|
1688
|
+
raise RoomException(
|
|
1689
|
+
f"Invalid room grants-by-room payload: {exc}"
|
|
1690
|
+
) from exc
|
|
1691
|
+
|
|
1692
|
+
async def list_unique_rooms_with_grants(
|
|
1693
|
+
self,
|
|
1694
|
+
*,
|
|
1695
|
+
project_id: str,
|
|
1696
|
+
limit: int = 50,
|
|
1697
|
+
offset: int = 0,
|
|
1698
|
+
) -> List[ProjectRoomGrantCount]:
|
|
1699
|
+
"""
|
|
1700
|
+
GET /accounts/projects/{project_id}/room-grants/by-room?limit=&offset=
|
|
1701
|
+
Returns [ProjectRoomGrantCount]; accepts either {"room": "..."} or {"room_name": "..."} shapes.
|
|
1702
|
+
"""
|
|
1703
|
+
params = {"limit": str(limit), "offset": str(offset)}
|
|
1704
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/room-grants/by-room"
|
|
1705
|
+
async with self._session.get(
|
|
1706
|
+
url, headers=self._get_headers(), params=params
|
|
1707
|
+
) as resp:
|
|
1708
|
+
await self._raise_for_status(resp)
|
|
1709
|
+
data = await resp.json()
|
|
1710
|
+
items = data.get("rooms", [])
|
|
1711
|
+
out: List[ProjectRoomGrantCount] = []
|
|
1712
|
+
for item in items:
|
|
1713
|
+
# tolerate either key name
|
|
1714
|
+
out.append(ProjectRoomGrantCount.model_validate(item))
|
|
1715
|
+
return out
|
|
1716
|
+
|
|
1717
|
+
async def list_unique_users_with_grants(
|
|
1718
|
+
self,
|
|
1719
|
+
*,
|
|
1720
|
+
project_id: str,
|
|
1721
|
+
limit: int = 50,
|
|
1722
|
+
offset: int = 0,
|
|
1723
|
+
) -> List[ProjectUserGrantCount]:
|
|
1724
|
+
"""
|
|
1725
|
+
GET /accounts/projects/{project_id}/room-grants/by-user?limit=&offset=
|
|
1726
|
+
Returns [ProjectUserGrantCount]
|
|
1727
|
+
"""
|
|
1728
|
+
params = {"limit": str(limit), "offset": str(offset)}
|
|
1729
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/room-grants/by-user"
|
|
1730
|
+
async with self._session.get(
|
|
1731
|
+
url, headers=self._get_headers(), params=params
|
|
1732
|
+
) as resp:
|
|
1733
|
+
await self._raise_for_status(resp)
|
|
1734
|
+
data = await resp.json()
|
|
1735
|
+
items = data.get("users", [])
|
|
1736
|
+
out: List[ProjectUserGrantCount] = []
|
|
1737
|
+
for item in items:
|
|
1738
|
+
out.append(ProjectUserGrantCount.model_validate(item))
|
|
1739
|
+
return out
|
|
1740
|
+
|
|
1741
|
+
async def create_oauth_client(
|
|
1742
|
+
self,
|
|
1743
|
+
*,
|
|
1744
|
+
project_id: str,
|
|
1745
|
+
grant_types: List[str],
|
|
1746
|
+
response_types: List[str],
|
|
1747
|
+
redirect_uris: List[str],
|
|
1748
|
+
scope: str,
|
|
1749
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1750
|
+
official: bool = False,
|
|
1751
|
+
) -> OAuthClient:
|
|
1752
|
+
"""
|
|
1753
|
+
POST /accounts/projects/{project_id}/oauth/clients
|
|
1754
|
+
Body: { grant_types, response_types, redirect_uris, scope, metadata? }
|
|
1755
|
+
Returns the newly created client (including client_secret).
|
|
1756
|
+
"""
|
|
1757
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/oauth/clients"
|
|
1758
|
+
payload = {
|
|
1759
|
+
"grant_types": grant_types,
|
|
1760
|
+
"response_types": response_types,
|
|
1761
|
+
"redirect_uris": redirect_uris,
|
|
1762
|
+
"scope": scope,
|
|
1763
|
+
"metadata": metadata or {},
|
|
1764
|
+
"official": official,
|
|
1765
|
+
}
|
|
1766
|
+
async with self._session.post(
|
|
1767
|
+
url, headers=self._get_headers(), json=payload
|
|
1768
|
+
) as resp:
|
|
1769
|
+
await self._raise_for_status(resp)
|
|
1770
|
+
raw = await resp.json()
|
|
1771
|
+
try:
|
|
1772
|
+
return OAuthClient.model_validate(raw)
|
|
1773
|
+
except ValidationError as exc:
|
|
1774
|
+
raise RoomException(
|
|
1775
|
+
f"Invalid create-oauth-client payload: {exc}"
|
|
1776
|
+
) from exc
|
|
1777
|
+
|
|
1778
|
+
async def update_oauth_client(
|
|
1779
|
+
self,
|
|
1780
|
+
*,
|
|
1781
|
+
project_id: str,
|
|
1782
|
+
client_id: str,
|
|
1783
|
+
grant_types: Optional[List[str]] = None,
|
|
1784
|
+
response_types: Optional[List[str]] = None,
|
|
1785
|
+
redirect_uris: Optional[List[str]] = None,
|
|
1786
|
+
scope: Optional[str] = None,
|
|
1787
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1788
|
+
official: Optional[bool] = None,
|
|
1789
|
+
) -> Dict[str, Any]:
|
|
1790
|
+
"""
|
|
1791
|
+
PUT /accounts/projects/{project_id}/oauth/clients/{client_id}
|
|
1792
|
+
Body: any subset of { grant_types, response_types, redirect_uris, scope, metadata }
|
|
1793
|
+
Returns { "ok": True } on success.
|
|
1794
|
+
"""
|
|
1795
|
+
url = (
|
|
1796
|
+
f"{self.base_url}/accounts/projects/{project_id}/oauth/clients/{client_id}"
|
|
1797
|
+
)
|
|
1798
|
+
body: Dict[str, Any] = {}
|
|
1799
|
+
if grant_types is not None:
|
|
1800
|
+
body["grant_types"] = grant_types
|
|
1801
|
+
if response_types is not None:
|
|
1802
|
+
body["response_types"] = response_types
|
|
1803
|
+
if redirect_uris is not None:
|
|
1804
|
+
body["redirect_uris"] = redirect_uris
|
|
1805
|
+
if scope is not None:
|
|
1806
|
+
body["scope"] = scope
|
|
1807
|
+
if metadata is not None:
|
|
1808
|
+
body["metadata"] = metadata
|
|
1809
|
+
if official is not None:
|
|
1810
|
+
body["official"] = official
|
|
1811
|
+
|
|
1812
|
+
async with self._session.put(
|
|
1813
|
+
url, headers=self._get_headers(), json=body
|
|
1814
|
+
) as resp:
|
|
1815
|
+
await self._raise_for_status(resp)
|
|
1816
|
+
return await resp.json()
|
|
1817
|
+
|
|
1818
|
+
async def list_oauth_clients(self, *, project_id: str) -> List[OAuthClient]:
|
|
1819
|
+
"""
|
|
1820
|
+
GET /accounts/projects/{project_id}/oauth/clients
|
|
1821
|
+
Returns a list of OAuthClient (no secrets).
|
|
1822
|
+
"""
|
|
1823
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/oauth/clients"
|
|
1824
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1825
|
+
await self._raise_for_status(resp)
|
|
1826
|
+
raw = await resp.json()
|
|
1827
|
+
try:
|
|
1828
|
+
return [
|
|
1829
|
+
OAuthClient.model_validate(item) for item in raw.get("clients", [])
|
|
1830
|
+
]
|
|
1831
|
+
except ValidationError as exc:
|
|
1832
|
+
raise RoomException(
|
|
1833
|
+
f"Invalid oauth-clients list payload: {exc}"
|
|
1834
|
+
) from exc
|
|
1835
|
+
|
|
1836
|
+
async def get_oauth_client(
|
|
1837
|
+
self, *, project_id: Optional[str], client_id: str
|
|
1838
|
+
) -> OAuthClient:
|
|
1839
|
+
"""
|
|
1840
|
+
GET /accounts/projects/{project_id}/oauth/clients/{client_id}
|
|
1841
|
+
Returns the OAuthClient (no secret).
|
|
1842
|
+
"""
|
|
1843
|
+
|
|
1844
|
+
url = (
|
|
1845
|
+
f"{self.base_url}/accounts/projects/{project_id}/oauth/clients/{client_id}"
|
|
1846
|
+
)
|
|
1847
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1848
|
+
if resp.status == 404:
|
|
1849
|
+
raise RoomException("oauth client not found")
|
|
1850
|
+
await self._raise_for_status(resp)
|
|
1851
|
+
raw = await resp.json()
|
|
1852
|
+
try:
|
|
1853
|
+
return OAuthClient.model_validate(raw)
|
|
1854
|
+
except ValidationError as exc:
|
|
1855
|
+
raise RoomException(f"Invalid oauth-client payload: {exc}") from exc
|
|
1856
|
+
|
|
1857
|
+
async def delete_oauth_client(self, *, project_id: str, client_id: str) -> None:
|
|
1858
|
+
"""
|
|
1859
|
+
DELETE /accounts/projects/{project_id}/oauth/clients/{client_id}
|
|
1860
|
+
Returns 204 No Content on success.
|
|
1861
|
+
"""
|
|
1862
|
+
url = (
|
|
1863
|
+
f"{self.base_url}/accounts/projects/{project_id}/oauth/clients/{client_id}"
|
|
1864
|
+
)
|
|
1865
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1866
|
+
await self._raise_for_status(resp)
|
|
1867
|
+
|
|
1868
|
+
# ---------------------------
|
|
1869
|
+
# Scheduled Tasks
|
|
1870
|
+
# ---------------------------
|
|
1871
|
+
|
|
1872
|
+
async def create_scheduled_task(
|
|
1873
|
+
self,
|
|
1874
|
+
*,
|
|
1875
|
+
project_id: str,
|
|
1876
|
+
room_name: str,
|
|
1877
|
+
queue_name: str,
|
|
1878
|
+
payload: Any,
|
|
1879
|
+
schedule: str,
|
|
1880
|
+
active: bool = True,
|
|
1881
|
+
task_id: Optional[str] = None,
|
|
1882
|
+
once: bool = False,
|
|
1883
|
+
annotations: Optional[dict[str, str]] = None,
|
|
1884
|
+
) -> str:
|
|
1885
|
+
"""
|
|
1886
|
+
POST /accounts/projects/{project_id}/scheduled-tasks
|
|
1887
|
+
|
|
1888
|
+
payload can be dict (preferred) or json-string.
|
|
1889
|
+
Returns the created ScheduledTask when the server returns it.
|
|
1890
|
+
"""
|
|
1891
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/scheduled-tasks"
|
|
1892
|
+
|
|
1893
|
+
body = _CreateScheduledTaskRequest(
|
|
1894
|
+
id=task_id,
|
|
1895
|
+
room_name=room_name,
|
|
1896
|
+
queue_name=queue_name,
|
|
1897
|
+
payload=payload,
|
|
1898
|
+
schedule=schedule,
|
|
1899
|
+
active=active,
|
|
1900
|
+
once=once,
|
|
1901
|
+
annotations=annotations,
|
|
1902
|
+
).model_dump(mode="json", exclude_none=True)
|
|
1903
|
+
|
|
1904
|
+
async with self._session.post(
|
|
1905
|
+
url, headers=self._get_headers(), json=body
|
|
1906
|
+
) as resp:
|
|
1907
|
+
await self._ensure_success(resp, action="create scheduled task")
|
|
1908
|
+
data = await resp.json()
|
|
1909
|
+
return data["task_id"]
|
|
1910
|
+
|
|
1911
|
+
async def update_scheduled_task(
|
|
1912
|
+
self,
|
|
1913
|
+
*,
|
|
1914
|
+
project_id: str,
|
|
1915
|
+
task_id: str,
|
|
1916
|
+
room_name: Optional[str] = None,
|
|
1917
|
+
queue_name: Optional[str] = None,
|
|
1918
|
+
payload: Optional[Any] = None,
|
|
1919
|
+
schedule: Optional[str] = None,
|
|
1920
|
+
active: Optional[bool] = None,
|
|
1921
|
+
annotations: Optional[dict[str, str]] = None,
|
|
1922
|
+
) -> None:
|
|
1923
|
+
"""
|
|
1924
|
+
PUT /accounts/projects/{project_id}/scheduled-tasks/{task_id}
|
|
1925
|
+
|
|
1926
|
+
Patch-like update. Any omitted fields are left unchanged.
|
|
1927
|
+
Returns the updated ScheduledTask when the server returns it; otherwise fetches it.
|
|
1928
|
+
"""
|
|
1929
|
+
url = (
|
|
1930
|
+
f"{self.base_url}/accounts/projects/{project_id}/scheduled-tasks/{task_id}"
|
|
1931
|
+
)
|
|
1932
|
+
|
|
1933
|
+
body = _UpdateScheduledTaskRequest(
|
|
1934
|
+
room_name=room_name,
|
|
1935
|
+
queue_name=queue_name,
|
|
1936
|
+
payload=payload,
|
|
1937
|
+
schedule=schedule,
|
|
1938
|
+
active=active,
|
|
1939
|
+
annotations=annotations,
|
|
1940
|
+
).model_dump(mode="json", exclude_none=True)
|
|
1941
|
+
|
|
1942
|
+
async with self._session.put(
|
|
1943
|
+
url, headers=self._get_headers(), json=body
|
|
1944
|
+
) as resp:
|
|
1945
|
+
await self._ensure_success(resp, action="update scheduled task")
|
|
1946
|
+
|
|
1947
|
+
async def delete_scheduled_task(self, *, project_id: str, task_id: str) -> None:
|
|
1948
|
+
"""
|
|
1949
|
+
DELETE /accounts/projects/{project_id}/scheduled-tasks/{task_id}
|
|
1950
|
+
Returns 204 No Content on success.
|
|
1951
|
+
"""
|
|
1952
|
+
url = (
|
|
1953
|
+
f"{self.base_url}/accounts/projects/{project_id}/scheduled-tasks/{task_id}"
|
|
1954
|
+
)
|
|
1955
|
+
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1956
|
+
await self._ensure_success(resp, action="delete scheduled task")
|
|
1957
|
+
return None
|
|
1958
|
+
|
|
1959
|
+
async def list_scheduled_tasks(
|
|
1960
|
+
self,
|
|
1961
|
+
*,
|
|
1962
|
+
project_id: str,
|
|
1963
|
+
room_name: Optional[str] = None,
|
|
1964
|
+
task_id: Optional[str] = None,
|
|
1965
|
+
active: Optional[bool] = None,
|
|
1966
|
+
limit: int = 200,
|
|
1967
|
+
offset: int = 0,
|
|
1968
|
+
) -> List[ScheduledTask]:
|
|
1969
|
+
"""
|
|
1970
|
+
GET /accounts/projects/{project_id}/scheduled-tasks?room_name=&task_id=&active=&limit=&offset=
|
|
1971
|
+
Returns a list[ScheduledTask].
|
|
1972
|
+
"""
|
|
1973
|
+
url = f"{self.base_url}/accounts/projects/{project_id}/scheduled-tasks"
|
|
1974
|
+
params: Dict[str, str] = {
|
|
1975
|
+
"limit": str(limit),
|
|
1976
|
+
"offset": str(offset),
|
|
1977
|
+
}
|
|
1978
|
+
if room_name is not None:
|
|
1979
|
+
params["room_name"] = room_name
|
|
1980
|
+
if task_id is not None:
|
|
1981
|
+
params["task_id"] = task_id
|
|
1982
|
+
if active is not None:
|
|
1983
|
+
params["active"] = "true" if active else "false"
|
|
1984
|
+
|
|
1985
|
+
async with self._session.get(
|
|
1986
|
+
url, headers=self._get_headers(), params=params
|
|
1987
|
+
) as resp:
|
|
1988
|
+
await self._ensure_success(resp, action="list scheduled tasks")
|
|
1989
|
+
data = await resp.json()
|
|
1990
|
+
|
|
1991
|
+
tasks_raw = data.get("tasks", [])
|
|
1992
|
+
if not isinstance(tasks_raw, list):
|
|
1993
|
+
raise RoomException(
|
|
1994
|
+
"Invalid scheduled-tasks payload: expected 'tasks' to be a list"
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
try:
|
|
1998
|
+
return [ScheduledTask.model_validate(item) for item in tasks_raw]
|
|
1999
|
+
except ValidationError as exc:
|
|
2000
|
+
raise RoomException(f"Invalid scheduled-tasks payload: {exc}") from exc
|