eduskit 0.1.4__tar.gz → 0.1.6__tar.gz
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.
- {eduskit-0.1.4 → eduskit-0.1.6}/PKG-INFO +24 -2
- {eduskit-0.1.4 → eduskit-0.1.6}/README.md +23 -1
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit/__init__.py +10 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit/classroom.py +51 -2
- eduskit-0.1.6/eduskit/classroom_media.py +120 -0
- eduskit-0.1.6/eduskit/console.py +60 -0
- eduskit-0.1.6/eduskit/console.pyi +31 -0
- eduskit-0.1.6/eduskit/console_api.py +407 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit/http.py +5 -1
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit/whiteboard.py +43 -1
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit.egg-info/PKG-INFO +24 -2
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit.egg-info/SOURCES.txt +5 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/pyproject.toml +1 -1
- eduskit-0.1.6/tests/test_console.py +101 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/tests/test_eduskit.py +38 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/LICENSE +0 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit/errors.py +0 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit/token.py +0 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit.egg-info/dependency_links.txt +0 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/eduskit.egg-info/top_level.txt +0 -0
- {eduskit-0.1.4 → eduskit-0.1.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eduskit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Eduskit B-end server SDK for classroom and whiteboard APIs
|
|
5
5
|
Author: Eduskit
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -68,7 +68,7 @@ token = sdk.client.auth.issue_token(eduUserId=user["eduUserId"], originId="stu_0
|
|
|
68
68
|
classroom = sdk.client.classrooms.create(
|
|
69
69
|
name="一年级数学",
|
|
70
70
|
startsAt="2026-08-17T10:00:00.000Z",
|
|
71
|
-
|
|
71
|
+
durationMinutes=60,
|
|
72
72
|
teacherEduUserId=user["eduUserId"],
|
|
73
73
|
)
|
|
74
74
|
sdk.client.classrooms.start(classroom["classroomId"])
|
|
@@ -144,3 +144,25 @@ except EduskitError as e:
|
|
|
144
144
|
## 示例
|
|
145
145
|
|
|
146
146
|
- [examples/classroom_quickstart.py](examples/classroom_quickstart.py)
|
|
147
|
+
|
|
148
|
+
## Application business APIs
|
|
149
|
+
|
|
150
|
+
Use the application-credential `client` for user lists and status, classroom queries and updates, courseware libraries, live assets, attendance, entry diagnostics, and activity logs. No console tenant token is required. See [the complete API reference](https://docs.eduskit.com/server-sdk/business-api/). New methods require a matching server-api deployment and a source SDK build.
|
|
151
|
+
|
|
152
|
+
## Console management
|
|
153
|
+
|
|
154
|
+
`ConsoleClient` exposes tenant management, authentication, applications, users, classrooms, courseware, live video, diagnostics, dashboards, and billing. Use the console-api URL and a tenant access token. Application-scoped methods require `appId`; app credentials are not sent by this client. This API is available in this source build.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import os
|
|
158
|
+
from eduskit import Eduskit
|
|
159
|
+
sdk = Eduskit(console_client={
|
|
160
|
+
"base_url": os.environ["EDU_CONSOLE_URL"],
|
|
161
|
+
"access_token": os.environ["EDU_TENANT_ACCESS_TOKEN"],
|
|
162
|
+
})
|
|
163
|
+
c = sdk.console_client
|
|
164
|
+
apps = c.apps.list()
|
|
165
|
+
users = c.users.list(os.environ["EDU_APP_ID"], page=1, pageSize=20)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Full API fields and workflows: [Console management](https://docs.eduskit.com/server-sdk/console/).
|
|
@@ -55,7 +55,7 @@ token = sdk.client.auth.issue_token(eduUserId=user["eduUserId"], originId="stu_0
|
|
|
55
55
|
classroom = sdk.client.classrooms.create(
|
|
56
56
|
name="一年级数学",
|
|
57
57
|
startsAt="2026-08-17T10:00:00.000Z",
|
|
58
|
-
|
|
58
|
+
durationMinutes=60,
|
|
59
59
|
teacherEduUserId=user["eduUserId"],
|
|
60
60
|
)
|
|
61
61
|
sdk.client.classrooms.start(classroom["classroomId"])
|
|
@@ -131,3 +131,25 @@ except EduskitError as e:
|
|
|
131
131
|
## 示例
|
|
132
132
|
|
|
133
133
|
- [examples/classroom_quickstart.py](examples/classroom_quickstart.py)
|
|
134
|
+
|
|
135
|
+
## Application business APIs
|
|
136
|
+
|
|
137
|
+
Use the application-credential `client` for user lists and status, classroom queries and updates, courseware libraries, live assets, attendance, entry diagnostics, and activity logs. No console tenant token is required. See [the complete API reference](https://docs.eduskit.com/server-sdk/business-api/). New methods require a matching server-api deployment and a source SDK build.
|
|
138
|
+
|
|
139
|
+
## Console management
|
|
140
|
+
|
|
141
|
+
`ConsoleClient` exposes tenant management, authentication, applications, users, classrooms, courseware, live video, diagnostics, dashboards, and billing. Use the console-api URL and a tenant access token. Application-scoped methods require `appId`; app credentials are not sent by this client. This API is available in this source build.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import os
|
|
145
|
+
from eduskit import Eduskit
|
|
146
|
+
sdk = Eduskit(console_client={
|
|
147
|
+
"base_url": os.environ["EDU_CONSOLE_URL"],
|
|
148
|
+
"access_token": os.environ["EDU_TENANT_ACCESS_TOKEN"],
|
|
149
|
+
})
|
|
150
|
+
c = sdk.console_client
|
|
151
|
+
apps = c.apps.list()
|
|
152
|
+
users = c.users.list(os.environ["EDU_APP_ID"], page=1, pageSize=20)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Full API fields and workflows: [Console management](https://docs.eduskit.com/server-sdk/console/).
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
5
|
+
from .console import ConsoleClient
|
|
5
6
|
from .classroom import ClassroomClient
|
|
6
7
|
from .errors import EduskitError
|
|
7
8
|
from .http import Fetcher, HttpTransport
|
|
@@ -9,6 +10,7 @@ from .whiteboard import WhiteboardClient
|
|
|
9
10
|
|
|
10
11
|
__all__ = [
|
|
11
12
|
"Eduskit",
|
|
13
|
+
"ConsoleClient",
|
|
12
14
|
"EduskitError",
|
|
13
15
|
"ClassroomClient",
|
|
14
16
|
"WhiteboardClient",
|
|
@@ -21,10 +23,12 @@ class Eduskit:
|
|
|
21
23
|
*,
|
|
22
24
|
client: Optional[Dict[str, Any]] = None,
|
|
23
25
|
whiteboard_client: Optional[Dict[str, Any]] = None,
|
|
26
|
+
console_client: Optional[Dict[str, Any]] = None,
|
|
24
27
|
timeout_ms: int = 10_000,
|
|
25
28
|
lang: str = "zh-CN",
|
|
26
29
|
fetch: Optional[Fetcher] = None,
|
|
27
30
|
) -> None:
|
|
31
|
+
self._console = ConsoleClient(**{"timeout_ms": timeout_ms, "lang": lang, "fetch": fetch, **console_client}) if console_client is not None else None
|
|
28
32
|
self._client: Optional[ClassroomClient] = None
|
|
29
33
|
self._whiteboard: Optional[WhiteboardClient] = None
|
|
30
34
|
if client:
|
|
@@ -75,3 +79,9 @@ class Eduskit:
|
|
|
75
79
|
source="whiteboard",
|
|
76
80
|
)
|
|
77
81
|
return self._whiteboard
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def console_client(self) -> ConsoleClient:
|
|
85
|
+
if self._console is None:
|
|
86
|
+
raise EduskitError("console client is not configured", error_code="SDK_CLIENT_NOT_CONFIGURED", source="console")
|
|
87
|
+
return self._console
|
|
@@ -4,11 +4,12 @@ from urllib.parse import urlencode
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from .classroom_media import LiveVideosApi, ClassroomMediaApi, ServerUsersApi, ServerClassroomsApi, CoursewaresApi, ActivityApi
|
|
7
8
|
from .http import HttpTransport
|
|
8
9
|
from .token import iso_time, sign_hs256
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class _Users:
|
|
12
|
+
class _Users(ServerUsersApi):
|
|
12
13
|
def __init__(self, http: HttpTransport) -> None:
|
|
13
14
|
self._http = http
|
|
14
15
|
|
|
@@ -110,11 +111,44 @@ class _Coursewares:
|
|
|
110
111
|
)
|
|
111
112
|
|
|
112
113
|
|
|
113
|
-
class
|
|
114
|
+
class _EntryCode:
|
|
115
|
+
def __init__(self, http: HttpTransport) -> None:
|
|
116
|
+
self._http = http
|
|
117
|
+
|
|
118
|
+
def list(self, classroom_id: str) -> Any:
|
|
119
|
+
return self._http.request(
|
|
120
|
+
"GET",
|
|
121
|
+
"/v1/classrooms/entry-codes" + "?" + urlencode({"classroomId": classroom_id}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def issue(self, classroom_id: str, **input: Any) -> Any:
|
|
125
|
+
return self._http.request(
|
|
126
|
+
"POST",
|
|
127
|
+
"/v1/classrooms/entry-codes",
|
|
128
|
+
{**(input or {}), "classroomId": classroom_id},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def update(self, classroom_id: str, **input: Any) -> Any:
|
|
132
|
+
return self._http.request(
|
|
133
|
+
"POST",
|
|
134
|
+
"/v1/classrooms/entry-codes/update",
|
|
135
|
+
{**(input or {}), "classroomId": classroom_id},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def revoke(self, classroom_id: str, entry_code_id: str) -> Any:
|
|
139
|
+
return self._http.request(
|
|
140
|
+
"POST",
|
|
141
|
+
"/v1/classrooms/entry-codes/revoke",
|
|
142
|
+
{"classroomId": classroom_id, "entryCodeId": entry_code_id},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _Classrooms(ServerClassroomsApi):
|
|
114
147
|
def __init__(self, http: HttpTransport) -> None:
|
|
115
148
|
self.members = _Members(http)
|
|
116
149
|
self.permissions = _Permissions(http)
|
|
117
150
|
self.coursewares = _Coursewares(http)
|
|
151
|
+
self.entry_code = _EntryCode(http)
|
|
118
152
|
self._http = http
|
|
119
153
|
|
|
120
154
|
def create(self, **input: Any) -> Any:
|
|
@@ -140,12 +174,27 @@ class _App:
|
|
|
140
174
|
|
|
141
175
|
class ClassroomClient:
|
|
142
176
|
def __init__(self, http: HttpTransport, *, app_id: str, app_secret: str) -> None:
|
|
177
|
+
self.coursewares = CoursewaresApi(http)
|
|
178
|
+
self.activity = ActivityApi(http)
|
|
179
|
+
self.live_videos = LiveVideosApi(http)
|
|
180
|
+
self.classroom_media = ClassroomMediaApi(http)
|
|
143
181
|
self.users = _Users(http)
|
|
144
182
|
self.auth = _Auth(app_id, app_secret)
|
|
145
183
|
self.classrooms = _Classrooms(http)
|
|
146
184
|
self.app = _App(http)
|
|
147
185
|
self._http = http
|
|
148
186
|
|
|
187
|
+
def upload_file(self, url: str, data: bytes, content_type: str) -> None:
|
|
188
|
+
"""PUT bytes to a signed storage URL without tenant or app credentials."""
|
|
189
|
+
from .errors import EduskitError
|
|
190
|
+
from urllib.error import URLError
|
|
191
|
+
try:
|
|
192
|
+
status, _, _ = self._http.fetch(url, "PUT", {"Content-Type": content_type}, data, self._http.timeout_ms)
|
|
193
|
+
except (URLError, OSError) as error:
|
|
194
|
+
raise EduskitError(str(error), error_code="SDK_NETWORK_ERROR", source="classroom") from error
|
|
195
|
+
if not 200 <= status < 300:
|
|
196
|
+
raise EduskitError(f"upload failed (HTTP {status})", status=status, error_code="SDK_UPLOAD_FAILED", source="classroom")
|
|
197
|
+
|
|
149
198
|
@property
|
|
150
199
|
def last_trace_id(self) -> str:
|
|
151
200
|
return self._http.last_trace_id
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Generated by scripts/generate-classroom-media.py.
|
|
2
|
+
from urllib.parse import urlencode
|
|
3
|
+
from typing import Any
|
|
4
|
+
class ClassroomMediaApi:
|
|
5
|
+
def __init__(self, http):
|
|
6
|
+
self._http = http
|
|
7
|
+
|
|
8
|
+
def list_whiteboard_coursewares(self, **input: Any) -> Any:
|
|
9
|
+
return self._http.request("GET", "/v1/classrooms/whiteboard/coursewares" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
10
|
+
|
|
11
|
+
def add_whiteboard_courseware(self, **input: Any) -> Any:
|
|
12
|
+
return self._http.request("POST", "/v1/classrooms/whiteboard/coursewares", input)
|
|
13
|
+
|
|
14
|
+
def get_live_source(self, **input: Any) -> Any:
|
|
15
|
+
return self._http.request("GET", "/v1/classrooms/live-source" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
16
|
+
|
|
17
|
+
def set_live_source(self, **input: Any) -> Any:
|
|
18
|
+
return self._http.request("PUT", "/v1/classrooms/live-source", input)
|
|
19
|
+
|
|
20
|
+
def issue_live_push_credential(self, **input: Any) -> Any:
|
|
21
|
+
return self._http.request("POST", "/v1/classrooms/live-source/push-credential", input)
|
|
22
|
+
|
|
23
|
+
def get_live_play_credentials(self, **input: Any) -> Any:
|
|
24
|
+
return self._http.request("GET", "/v1/classrooms/live/play-credentials" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
25
|
+
|
|
26
|
+
class LiveVideosApi:
|
|
27
|
+
def __init__(self, http):
|
|
28
|
+
self._http = http
|
|
29
|
+
|
|
30
|
+
def list(self, **input: Any) -> Any:
|
|
31
|
+
return self._http.request("GET", "/v1/live-videos" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
32
|
+
|
|
33
|
+
def create_upload_url(self, **input: Any) -> Any:
|
|
34
|
+
return self._http.request("POST", "/v1/live-videos/upload-url", input)
|
|
35
|
+
|
|
36
|
+
def create(self, **input: Any) -> Any:
|
|
37
|
+
return self._http.request("POST", "/v1/live-videos", input)
|
|
38
|
+
|
|
39
|
+
def remove(self, **input: Any) -> Any:
|
|
40
|
+
return self._http.request("DELETE", "/v1/live-videos/remove" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
41
|
+
|
|
42
|
+
class ServerUsersApi:
|
|
43
|
+
def __init__(self, http):
|
|
44
|
+
self._http = http
|
|
45
|
+
|
|
46
|
+
def list(self, **input: Any) -> Any:
|
|
47
|
+
return self._http.request("GET", "/v1/users" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
48
|
+
|
|
49
|
+
def search(self, **input: Any) -> Any:
|
|
50
|
+
return self._http.request("GET", "/v1/users/search" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
51
|
+
|
|
52
|
+
def set_status(self, **input: Any) -> Any:
|
|
53
|
+
return self._http.request("PATCH", "/v1/users/status", input)
|
|
54
|
+
|
|
55
|
+
class ServerClassroomsApi:
|
|
56
|
+
def __init__(self, http):
|
|
57
|
+
self._http = http
|
|
58
|
+
|
|
59
|
+
def list(self, **input: Any) -> Any:
|
|
60
|
+
return self._http.request("GET", "/v1/classrooms" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
61
|
+
|
|
62
|
+
def get(self, **input: Any) -> Any:
|
|
63
|
+
return self._http.request("GET", "/v1/classrooms/detail" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
64
|
+
|
|
65
|
+
def update(self, **input: Any) -> Any:
|
|
66
|
+
return self._http.request("PATCH", "/v1/classrooms/update", input)
|
|
67
|
+
|
|
68
|
+
def attendance(self, **input: Any) -> Any:
|
|
69
|
+
return self._http.request("GET", "/v1/classrooms/attendance" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
70
|
+
|
|
71
|
+
def list_entered_users(self, **input: Any) -> Any:
|
|
72
|
+
return self._http.request("GET", "/v1/classrooms/entered-users" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
73
|
+
|
|
74
|
+
def list_join_attempts(self, **input: Any) -> Any:
|
|
75
|
+
return self._http.request("GET", "/v1/classrooms/join-attempts" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
76
|
+
|
|
77
|
+
def get_entered_user_trace(self, **input: Any) -> Any:
|
|
78
|
+
return self._http.request("GET", "/v1/classrooms/entered-users/trace" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
79
|
+
|
|
80
|
+
class ActivityApi:
|
|
81
|
+
def __init__(self, http):
|
|
82
|
+
self._http = http
|
|
83
|
+
|
|
84
|
+
def list(self, **input: Any) -> Any:
|
|
85
|
+
return self._http.request("GET", "/v1/classrooms/activity" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
86
|
+
|
|
87
|
+
def tracks(self, **input: Any) -> Any:
|
|
88
|
+
return self._http.request("GET", "/v1/classrooms/activity/tracks" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
89
|
+
|
|
90
|
+
def overview(self, **input: Any) -> Any:
|
|
91
|
+
return self._http.request("GET", "/v1/classrooms/activity/overview" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
92
|
+
|
|
93
|
+
def detail(self, **input: Any) -> Any:
|
|
94
|
+
return self._http.request("GET", "/v1/classrooms/activity/detail" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
95
|
+
|
|
96
|
+
class CoursewaresApi:
|
|
97
|
+
def __init__(self, http):
|
|
98
|
+
self._http = http
|
|
99
|
+
|
|
100
|
+
def list(self, **input: Any) -> Any:
|
|
101
|
+
return self._http.request("GET", "/v1/coursewares" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
102
|
+
|
|
103
|
+
def create_upload_url(self, **input: Any) -> Any:
|
|
104
|
+
return self._http.request("POST", "/v1/coursewares/upload-url", input)
|
|
105
|
+
|
|
106
|
+
def create(self, **input: Any) -> Any:
|
|
107
|
+
return self._http.request("POST", "/v1/coursewares", input)
|
|
108
|
+
|
|
109
|
+
def get(self, **input: Any) -> Any:
|
|
110
|
+
return self._http.request("GET", "/v1/coursewares/detail" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
111
|
+
|
|
112
|
+
def get_manifest(self, **input: Any) -> Any:
|
|
113
|
+
return self._http.request("GET", "/v1/coursewares/manifest" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
114
|
+
|
|
115
|
+
def remove(self, **input: Any) -> Any:
|
|
116
|
+
return self._http.request("DELETE", "/v1/coursewares/remove" + '?' + urlencode({k:(str(v).lower() if isinstance(v, bool) else v) for k,v in input.items() if v is not None and v != ''}))
|
|
117
|
+
|
|
118
|
+
def retry(self, **input: Any) -> Any:
|
|
119
|
+
return self._http.request("POST", "/v1/coursewares/retry", input)
|
|
120
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
from .http import HttpTransport, Fetcher
|
|
5
|
+
from .errors import EduskitError
|
|
6
|
+
from .console_api import create_apis
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConsoleClient:
|
|
10
|
+
"""Tenant console management. Access tokens are updated explicitly after refresh."""
|
|
11
|
+
def __init__(self, *, base_url: str, access_token: str = "", timeout_ms: int = 10_000,
|
|
12
|
+
lang: str = "zh-CN", fetch: Fetcher | None = None) -> None:
|
|
13
|
+
self._http = HttpTransport(base_url=base_url, app_key="", app_secret="", source="console",
|
|
14
|
+
timeout_ms=timeout_ms, lang=lang, fetch=fetch)
|
|
15
|
+
self._access_token = access_token
|
|
16
|
+
for name, api in create_apis(self).items():
|
|
17
|
+
setattr(self, name, api)
|
|
18
|
+
|
|
19
|
+
def upload_file(self, url: str, data: bytes, content_type: str) -> None:
|
|
20
|
+
"""PUT bytes to a signed storage URL without tenant or app credentials."""
|
|
21
|
+
from urllib.error import URLError
|
|
22
|
+
try:
|
|
23
|
+
status, _, _ = self._http.fetch(url, "PUT", {"Content-Type": content_type}, data, self._http.timeout_ms)
|
|
24
|
+
except (URLError, OSError) as error:
|
|
25
|
+
raise EduskitError(str(error), error_code="SDK_NETWORK_ERROR", source="console") from error
|
|
26
|
+
if not 200 <= status < 300:
|
|
27
|
+
raise EduskitError(f"upload failed (HTTP {status})", status=status, error_code="SDK_UPLOAD_FAILED", source="console")
|
|
28
|
+
|
|
29
|
+
def set_access_token(self, token: str) -> None:
|
|
30
|
+
self._access_token = token
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def last_trace_id(self) -> str:
|
|
34
|
+
return self._http.last_trace_id
|
|
35
|
+
|
|
36
|
+
def _request(self, method: str, path: str, scope: str, input: dict[str, Any],
|
|
37
|
+
app_id: str | None = None, query_keys: list[str] | None = None) -> Any:
|
|
38
|
+
headers = {}
|
|
39
|
+
if scope != "public":
|
|
40
|
+
if not self._access_token.strip():
|
|
41
|
+
raise EduskitError("tenant access_token is required", error_code="SDK_CONSOLE_TOKEN_REQUIRED", source="console")
|
|
42
|
+
headers["Authorization"] = "Bearer " + self._access_token
|
|
43
|
+
if scope == "app":
|
|
44
|
+
if not app_id or not app_id.strip():
|
|
45
|
+
raise EduskitError("app_id is required", error_code="SDK_CONSOLE_APP_ID_REQUIRED", source="console")
|
|
46
|
+
headers["x-app-id"] = app_id
|
|
47
|
+
query, body = {}, {}
|
|
48
|
+
for key, value in input.items():
|
|
49
|
+
in_query = key in query_keys if query_keys is not None else method in ("GET", "DELETE")
|
|
50
|
+
if in_query:
|
|
51
|
+
if value is None or value == "":
|
|
52
|
+
continue
|
|
53
|
+
if not isinstance(value, (str, int, float, bool)):
|
|
54
|
+
raise EduskitError(f"query field {key} must be scalar", error_code="SDK_QUERY_INVALID", source="console")
|
|
55
|
+
query[key] = str(value).lower() if isinstance(value, bool) else str(value)
|
|
56
|
+
else:
|
|
57
|
+
body[key] = value
|
|
58
|
+
if query:
|
|
59
|
+
path += "?" + urlencode(query)
|
|
60
|
+
return self._http.request(method, path, None if method == "GET" or (method == "DELETE" and query_keys is None) else body, headers)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from .http import Fetcher
|
|
3
|
+
from .console_api import (
|
|
4
|
+
ConsoleAuthApi,
|
|
5
|
+
ConsoleAppsApi,
|
|
6
|
+
ConsoleUsersApi,
|
|
7
|
+
ConsoleClassroomsApi,
|
|
8
|
+
ConsoleEntryCodesApi,
|
|
9
|
+
ConsoleActivityApi,
|
|
10
|
+
ConsoleCoursewaresApi,
|
|
11
|
+
ConsoleLiveVideosApi,
|
|
12
|
+
ConsoleDashboardApi,
|
|
13
|
+
ConsoleBillingApi,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class ConsoleClient:
|
|
17
|
+
auth: ConsoleAuthApi
|
|
18
|
+
apps: ConsoleAppsApi
|
|
19
|
+
users: ConsoleUsersApi
|
|
20
|
+
classrooms: ConsoleClassroomsApi
|
|
21
|
+
entry_codes: ConsoleEntryCodesApi
|
|
22
|
+
activity: ConsoleActivityApi
|
|
23
|
+
coursewares: ConsoleCoursewaresApi
|
|
24
|
+
live_videos: ConsoleLiveVideosApi
|
|
25
|
+
dashboard: ConsoleDashboardApi
|
|
26
|
+
billing: ConsoleBillingApi
|
|
27
|
+
def __init__(self, *, base_url: str, access_token: str = "", timeout_ms: int = 10000, lang: str = "zh-CN", fetch: Fetcher | None = None) -> None: ...
|
|
28
|
+
def set_access_token(self, token: str) -> None: ...
|
|
29
|
+
@property
|
|
30
|
+
def last_trace_id(self) -> str: ...
|
|
31
|
+
def upload_file(self, url: str, data: bytes, content_type: str) -> None: ...
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Generated by scripts/generate-console.py.
|
|
2
|
+
from typing import Any
|
|
3
|
+
class ConsoleAuthApi:
|
|
4
|
+
def __init__(self, transport):
|
|
5
|
+
self._transport = transport
|
|
6
|
+
|
|
7
|
+
def methods(self, **input: Any) -> Any:
|
|
8
|
+
"""Available sign-in methods. GET /v1/auth/methods."""
|
|
9
|
+
return self._transport._request("GET", "/v1/auth/methods", "public", input, None, None)
|
|
10
|
+
|
|
11
|
+
def send_sms(self, **input: Any) -> Any:
|
|
12
|
+
"""Send SMS code. POST /v1/auth/sms/send."""
|
|
13
|
+
return self._transport._request("POST", "/v1/auth/sms/send", "public", input, None, None)
|
|
14
|
+
|
|
15
|
+
def verify_sms(self, **input: Any) -> Any:
|
|
16
|
+
"""Sign in with SMS. POST /v1/auth/sms/verify."""
|
|
17
|
+
return self._transport._request("POST", "/v1/auth/sms/verify", "public", input, None, None)
|
|
18
|
+
|
|
19
|
+
def send_email(self, **input: Any) -> Any:
|
|
20
|
+
"""Send email code. POST /v1/auth/email/send."""
|
|
21
|
+
return self._transport._request("POST", "/v1/auth/email/send", "public", input, None, None)
|
|
22
|
+
|
|
23
|
+
def verify_email(self, **input: Any) -> Any:
|
|
24
|
+
"""Sign in with email. POST /v1/auth/email/verify."""
|
|
25
|
+
return self._transport._request("POST", "/v1/auth/email/verify", "public", input, None, None)
|
|
26
|
+
|
|
27
|
+
def google_authorize(self, **input: Any) -> Any:
|
|
28
|
+
"""Google authorization URL. GET /v1/auth/google/authorize."""
|
|
29
|
+
return self._transport._request("GET", "/v1/auth/google/authorize", "public", input, None, None)
|
|
30
|
+
|
|
31
|
+
def google_callback(self, **input: Any) -> Any:
|
|
32
|
+
"""Exchange Google authorization code. POST /v1/auth/google/callback."""
|
|
33
|
+
return self._transport._request("POST", "/v1/auth/google/callback", "public", input, None, None)
|
|
34
|
+
|
|
35
|
+
def google_token(self, **input: Any) -> Any:
|
|
36
|
+
"""Sign in with Google ID token. POST /v1/auth/google/token."""
|
|
37
|
+
return self._transport._request("POST", "/v1/auth/google/token", "public", input, None, None)
|
|
38
|
+
|
|
39
|
+
def wechat_authorize(self, **input: Any) -> Any:
|
|
40
|
+
"""WeChat authorization URL. GET /v1/auth/wechat/authorize."""
|
|
41
|
+
return self._transport._request("GET", "/v1/auth/wechat/authorize", "public", input, None, None)
|
|
42
|
+
|
|
43
|
+
def wechat_callback(self, **input: Any) -> Any:
|
|
44
|
+
"""Exchange WeChat authorization code. POST /v1/auth/wechat/callback."""
|
|
45
|
+
return self._transport._request("POST", "/v1/auth/wechat/callback", "public", input, None, None)
|
|
46
|
+
|
|
47
|
+
def send_wechat_bind_phone(self, **input: Any) -> Any:
|
|
48
|
+
"""Send phone code for WeChat sign-in. POST /v1/auth/wechat/bind-phone/send."""
|
|
49
|
+
return self._transport._request("POST", "/v1/auth/wechat/bind-phone/send", "public", input, None, None)
|
|
50
|
+
|
|
51
|
+
def verify_wechat_bind_phone(self, **input: Any) -> Any:
|
|
52
|
+
"""Verify phone for WeChat sign-in. POST /v1/auth/wechat/bind-phone/verify."""
|
|
53
|
+
return self._transport._request("POST", "/v1/auth/wechat/bind-phone/verify", "public", input, None, None)
|
|
54
|
+
|
|
55
|
+
def send_bind_phone(self, **input: Any) -> Any:
|
|
56
|
+
"""Send phone binding code. POST /v1/auth/phone/bind/send."""
|
|
57
|
+
return self._transport._request("POST", "/v1/auth/phone/bind/send", "tenant", input, None, None)
|
|
58
|
+
|
|
59
|
+
def verify_bind_phone(self, **input: Any) -> Any:
|
|
60
|
+
"""Bind phone to signed-in account. POST /v1/auth/phone/bind/verify."""
|
|
61
|
+
return self._transport._request("POST", "/v1/auth/phone/bind/verify", "tenant", input, None, None)
|
|
62
|
+
|
|
63
|
+
def wechat_bind_authorize(self, **input: Any) -> Any:
|
|
64
|
+
"""WeChat account binding URL. GET /v1/auth/wechat/bind/authorize."""
|
|
65
|
+
return self._transport._request("GET", "/v1/auth/wechat/bind/authorize", "tenant", input, None, None)
|
|
66
|
+
|
|
67
|
+
def wechat_bind_callback(self, **input: Any) -> Any:
|
|
68
|
+
"""Bind WeChat to account. POST /v1/auth/wechat/bind/callback."""
|
|
69
|
+
return self._transport._request("POST", "/v1/auth/wechat/bind/callback", "tenant", input, None, None)
|
|
70
|
+
|
|
71
|
+
def refresh(self, **input: Any) -> Any:
|
|
72
|
+
"""Refresh tenant session. POST /v1/auth/token/refresh."""
|
|
73
|
+
return self._transport._request("POST", "/v1/auth/token/refresh", "public", input, None, None)
|
|
74
|
+
|
|
75
|
+
def logout(self, **input: Any) -> Any:
|
|
76
|
+
"""Revoke tenant session. POST /v1/auth/logout."""
|
|
77
|
+
return self._transport._request("POST", "/v1/auth/logout", "public", input, None, None)
|
|
78
|
+
|
|
79
|
+
def me(self, **input: Any) -> Any:
|
|
80
|
+
"""Current account. GET /v1/auth/me."""
|
|
81
|
+
return self._transport._request("GET", "/v1/auth/me", "tenant", input, None, None)
|
|
82
|
+
|
|
83
|
+
class ConsoleAppsApi:
|
|
84
|
+
def __init__(self, transport):
|
|
85
|
+
self._transport = transport
|
|
86
|
+
|
|
87
|
+
def list(self, **input: Any) -> Any:
|
|
88
|
+
"""List applications. GET /v1/apps."""
|
|
89
|
+
return self._transport._request("GET", "/v1/apps", "tenant", input, None, None)
|
|
90
|
+
|
|
91
|
+
def get(self, **input: Any) -> Any:
|
|
92
|
+
"""Get application. GET /v1/apps/detail."""
|
|
93
|
+
return self._transport._request("GET", "/v1/apps/detail", "tenant", input, None, None)
|
|
94
|
+
|
|
95
|
+
def create(self, **input: Any) -> Any:
|
|
96
|
+
"""Create application. POST /v1/apps."""
|
|
97
|
+
return self._transport._request("POST", "/v1/apps", "tenant", input, None, None)
|
|
98
|
+
|
|
99
|
+
def update_config(self, **input: Any) -> Any:
|
|
100
|
+
"""Replace application configuration. PATCH /v1/apps/config."""
|
|
101
|
+
return self._transport._request("PATCH", "/v1/apps/config", "tenant", input, None, None)
|
|
102
|
+
|
|
103
|
+
def update_webhook(self, **input: Any) -> Any:
|
|
104
|
+
"""Configure webhook and signing secret. PATCH /v1/apps/webhook."""
|
|
105
|
+
return self._transport._request("PATCH", "/v1/apps/webhook", "tenant", input, None, None)
|
|
106
|
+
|
|
107
|
+
class ConsoleUsersApi:
|
|
108
|
+
def __init__(self, transport):
|
|
109
|
+
self._transport = transport
|
|
110
|
+
|
|
111
|
+
def list(self, app_id: str, **input: Any) -> Any:
|
|
112
|
+
"""List users. GET /v1/users."""
|
|
113
|
+
return self._transport._request("GET", "/v1/users", "app", input, app_id, None)
|
|
114
|
+
|
|
115
|
+
def search(self, app_id: str, **input: Any) -> Any:
|
|
116
|
+
"""Search users. GET /v1/users/search."""
|
|
117
|
+
return self._transport._request("GET", "/v1/users/search", "app", input, app_id, None)
|
|
118
|
+
|
|
119
|
+
def create(self, app_id: str, **input: Any) -> Any:
|
|
120
|
+
"""Create user. POST /v1/users."""
|
|
121
|
+
return self._transport._request("POST", "/v1/users", "app", input, app_id, None)
|
|
122
|
+
|
|
123
|
+
def set_status(self, app_id: str, **input: Any) -> Any:
|
|
124
|
+
"""Set user status. PATCH /v1/users/status."""
|
|
125
|
+
return self._transport._request("PATCH", "/v1/users/status", "app", input, app_id, None)
|
|
126
|
+
|
|
127
|
+
def issue_token(self, app_id: str, **input: Any) -> Any:
|
|
128
|
+
"""Issue user token. POST /v1/users/token."""
|
|
129
|
+
return self._transport._request("POST", "/v1/users/token", "app", input, app_id, None)
|
|
130
|
+
|
|
131
|
+
class ConsoleClassroomsApi:
|
|
132
|
+
def __init__(self, transport):
|
|
133
|
+
self._transport = transport
|
|
134
|
+
|
|
135
|
+
def list(self, app_id: str, **input: Any) -> Any:
|
|
136
|
+
"""List classrooms. GET /v1/classrooms."""
|
|
137
|
+
return self._transport._request("GET", "/v1/classrooms", "app", input, app_id, None)
|
|
138
|
+
|
|
139
|
+
def get(self, app_id: str, **input: Any) -> Any:
|
|
140
|
+
"""Get classroom. GET /v1/classrooms/detail."""
|
|
141
|
+
return self._transport._request("GET", "/v1/classrooms/detail", "app", input, app_id, None)
|
|
142
|
+
|
|
143
|
+
def create(self, app_id: str, **input: Any) -> Any:
|
|
144
|
+
"""Create classroom. POST /v1/classrooms."""
|
|
145
|
+
return self._transport._request("POST", "/v1/classrooms", "app", input, app_id, None)
|
|
146
|
+
|
|
147
|
+
def update(self, app_id: str, **input: Any) -> Any:
|
|
148
|
+
"""Update classroom. PATCH /v1/classrooms/update."""
|
|
149
|
+
return self._transport._request("PATCH", "/v1/classrooms/update", "app", input, app_id, None)
|
|
150
|
+
|
|
151
|
+
def start(self, app_id: str, **input: Any) -> Any:
|
|
152
|
+
"""Start classroom. POST /v1/classrooms/start."""
|
|
153
|
+
return self._transport._request("POST", "/v1/classrooms/start", "app", input, app_id, None)
|
|
154
|
+
|
|
155
|
+
def end(self, app_id: str, **input: Any) -> Any:
|
|
156
|
+
"""End classroom. POST /v1/classrooms/end."""
|
|
157
|
+
return self._transport._request("POST", "/v1/classrooms/end", "app", input, app_id, None)
|
|
158
|
+
|
|
159
|
+
def list_members(self, app_id: str, **input: Any) -> Any:
|
|
160
|
+
"""List classroom members. GET /v1/classrooms/members."""
|
|
161
|
+
return self._transport._request("GET", "/v1/classrooms/members", "app", input, app_id, None)
|
|
162
|
+
|
|
163
|
+
def add_member(self, app_id: str, **input: Any) -> Any:
|
|
164
|
+
"""Add classroom member. POST /v1/classrooms/members."""
|
|
165
|
+
return self._transport._request("POST", "/v1/classrooms/members", "app", input, app_id, None)
|
|
166
|
+
|
|
167
|
+
def replace_students(self, app_id: str, **input: Any) -> Any:
|
|
168
|
+
"""Replace student roster. PUT /v1/classrooms/members/students."""
|
|
169
|
+
return self._transport._request("PUT", "/v1/classrooms/members/students", "app", input, app_id, None)
|
|
170
|
+
|
|
171
|
+
def list_coursewares(self, app_id: str, **input: Any) -> Any:
|
|
172
|
+
"""List classroom courseware. GET /v1/classrooms/coursewares."""
|
|
173
|
+
return self._transport._request("GET", "/v1/classrooms/coursewares", "app", input, app_id, None)
|
|
174
|
+
|
|
175
|
+
def bind_coursewares(self, app_id: str, **input: Any) -> Any:
|
|
176
|
+
"""Bind classroom courseware. POST /v1/classrooms/coursewares."""
|
|
177
|
+
return self._transport._request("POST", "/v1/classrooms/coursewares", "app", input, app_id, None)
|
|
178
|
+
|
|
179
|
+
def unbind_coursewares(self, app_id: str, **input: Any) -> Any:
|
|
180
|
+
"""Unbind classroom courseware. DELETE /v1/classrooms/coursewares."""
|
|
181
|
+
return self._transport._request("DELETE", "/v1/classrooms/coursewares", "app", input, app_id, ['classroomId'])
|
|
182
|
+
|
|
183
|
+
def list_whiteboard_coursewares(self, app_id: str, **input: Any) -> Any:
|
|
184
|
+
"""List whiteboard courseware. GET /v1/classrooms/whiteboard/coursewares."""
|
|
185
|
+
return self._transport._request("GET", "/v1/classrooms/whiteboard/coursewares", "app", input, app_id, None)
|
|
186
|
+
|
|
187
|
+
def add_whiteboard_courseware(self, app_id: str, **input: Any) -> Any:
|
|
188
|
+
"""Add courseware to whiteboard. POST /v1/classrooms/whiteboard/coursewares."""
|
|
189
|
+
return self._transport._request("POST", "/v1/classrooms/whiteboard/coursewares", "app", input, app_id, None)
|
|
190
|
+
|
|
191
|
+
def attendance(self, app_id: str, **input: Any) -> Any:
|
|
192
|
+
"""Classroom attendance. GET /v1/classrooms/attendance."""
|
|
193
|
+
return self._transport._request("GET", "/v1/classrooms/attendance", "app", input, app_id, None)
|
|
194
|
+
|
|
195
|
+
def list_entered_users(self, app_id: str, **input: Any) -> Any:
|
|
196
|
+
"""List entered users. GET /v1/classrooms/entered-users."""
|
|
197
|
+
return self._transport._request("GET", "/v1/classrooms/entered-users", "app", input, app_id, None)
|
|
198
|
+
|
|
199
|
+
def list_join_attempts(self, app_id: str, **input: Any) -> Any:
|
|
200
|
+
"""List join attempts. GET /v1/classrooms/join-attempts."""
|
|
201
|
+
return self._transport._request("GET", "/v1/classrooms/join-attempts", "app", input, app_id, None)
|
|
202
|
+
|
|
203
|
+
def get_entered_user_trace(self, app_id: str, **input: Any) -> Any:
|
|
204
|
+
"""Get user entry trace. GET /v1/classrooms/entered-users/trace."""
|
|
205
|
+
return self._transport._request("GET", "/v1/classrooms/entered-users/trace", "app", input, app_id, None)
|
|
206
|
+
|
|
207
|
+
def get_live_source(self, app_id: str, **input: Any) -> Any:
|
|
208
|
+
"""Get live source. GET /v1/classrooms/live-source."""
|
|
209
|
+
return self._transport._request("GET", "/v1/classrooms/live-source", "app", input, app_id, None)
|
|
210
|
+
|
|
211
|
+
def set_live_source(self, app_id: str, **input: Any) -> Any:
|
|
212
|
+
"""Set live source. PUT /v1/classrooms/live-source."""
|
|
213
|
+
return self._transport._request("PUT", "/v1/classrooms/live-source", "app", input, app_id, None)
|
|
214
|
+
|
|
215
|
+
def issue_live_push_credential(self, app_id: str, **input: Any) -> Any:
|
|
216
|
+
"""Issue live push credential. POST /v1/classrooms/live-source/push-credential."""
|
|
217
|
+
return self._transport._request("POST", "/v1/classrooms/live-source/push-credential", "app", input, app_id, None)
|
|
218
|
+
|
|
219
|
+
def get_live_play_credentials(self, app_id: str, **input: Any) -> Any:
|
|
220
|
+
"""Get live playback credentials. GET /v1/classrooms/live/play-credentials."""
|
|
221
|
+
return self._transport._request("GET", "/v1/classrooms/live/play-credentials", "app", input, app_id, None)
|
|
222
|
+
|
|
223
|
+
class ConsoleEntryCodesApi:
|
|
224
|
+
def __init__(self, transport):
|
|
225
|
+
self._transport = transport
|
|
226
|
+
|
|
227
|
+
def list(self, app_id: str, **input: Any) -> Any:
|
|
228
|
+
"""List entry codes and claims. GET /v1/classrooms/entry-codes."""
|
|
229
|
+
return self._transport._request("GET", "/v1/classrooms/entry-codes", "app", input, app_id, None)
|
|
230
|
+
|
|
231
|
+
def issue(self, app_id: str, **input: Any) -> Any:
|
|
232
|
+
"""Issue entry codes. POST /v1/classrooms/entry-codes."""
|
|
233
|
+
return self._transport._request("POST", "/v1/classrooms/entry-codes", "app", input, app_id, None)
|
|
234
|
+
|
|
235
|
+
def update(self, app_id: str, **input: Any) -> Any:
|
|
236
|
+
"""Update or rotate entry code. POST /v1/classrooms/entry-codes/update."""
|
|
237
|
+
return self._transport._request("POST", "/v1/classrooms/entry-codes/update", "app", input, app_id, None)
|
|
238
|
+
|
|
239
|
+
def revoke(self, app_id: str, **input: Any) -> Any:
|
|
240
|
+
"""Revoke entry code. POST /v1/classrooms/entry-codes/revoke."""
|
|
241
|
+
return self._transport._request("POST", "/v1/classrooms/entry-codes/revoke", "app", input, app_id, None)
|
|
242
|
+
|
|
243
|
+
class ConsoleActivityApi:
|
|
244
|
+
def __init__(self, transport):
|
|
245
|
+
self._transport = transport
|
|
246
|
+
|
|
247
|
+
def list(self, app_id: str, **input: Any) -> Any:
|
|
248
|
+
"""List classroom activity. GET /v1/classrooms/activity."""
|
|
249
|
+
return self._transport._request("GET", "/v1/classrooms/activity", "app", input, app_id, None)
|
|
250
|
+
|
|
251
|
+
def tracks(self, app_id: str, **input: Any) -> Any:
|
|
252
|
+
"""Get activity tracks. GET /v1/classrooms/activity/tracks."""
|
|
253
|
+
return self._transport._request("GET", "/v1/classrooms/activity/tracks", "app", input, app_id, None)
|
|
254
|
+
|
|
255
|
+
def overview(self, app_id: str, **input: Any) -> Any:
|
|
256
|
+
"""Get activity overview. GET /v1/classrooms/activity/overview."""
|
|
257
|
+
return self._transport._request("GET", "/v1/classrooms/activity/overview", "app", input, app_id, None)
|
|
258
|
+
|
|
259
|
+
def detail(self, app_id: str, **input: Any) -> Any:
|
|
260
|
+
"""Get activity detail. GET /v1/classrooms/activity/detail."""
|
|
261
|
+
return self._transport._request("GET", "/v1/classrooms/activity/detail", "app", input, app_id, None)
|
|
262
|
+
|
|
263
|
+
class ConsoleCoursewaresApi:
|
|
264
|
+
def __init__(self, transport):
|
|
265
|
+
self._transport = transport
|
|
266
|
+
|
|
267
|
+
def list(self, app_id: str, **input: Any) -> Any:
|
|
268
|
+
"""List courseware library. GET /v1/coursewares."""
|
|
269
|
+
return self._transport._request("GET", "/v1/coursewares", "app", input, app_id, None)
|
|
270
|
+
|
|
271
|
+
def create_upload_url(self, app_id: str, **input: Any) -> Any:
|
|
272
|
+
"""Create courseware upload URL. POST /v1/coursewares/upload-url."""
|
|
273
|
+
return self._transport._request("POST", "/v1/coursewares/upload-url", "app", input, app_id, None)
|
|
274
|
+
|
|
275
|
+
def create(self, app_id: str, **input: Any) -> Any:
|
|
276
|
+
"""Create and convert courseware. POST /v1/coursewares."""
|
|
277
|
+
return self._transport._request("POST", "/v1/coursewares", "app", input, app_id, None)
|
|
278
|
+
|
|
279
|
+
def get(self, app_id: str, **input: Any) -> Any:
|
|
280
|
+
"""Get courseware. GET /v1/coursewares/detail."""
|
|
281
|
+
return self._transport._request("GET", "/v1/coursewares/detail", "app", input, app_id, None)
|
|
282
|
+
|
|
283
|
+
def get_manifest(self, app_id: str, **input: Any) -> Any:
|
|
284
|
+
"""Get courseware manifest. GET /v1/coursewares/manifest."""
|
|
285
|
+
return self._transport._request("GET", "/v1/coursewares/manifest", "app", input, app_id, None)
|
|
286
|
+
|
|
287
|
+
def remove(self, app_id: str, **input: Any) -> Any:
|
|
288
|
+
"""Delete courseware. DELETE /v1/coursewares/remove."""
|
|
289
|
+
return self._transport._request("DELETE", "/v1/coursewares/remove", "app", input, app_id, None)
|
|
290
|
+
|
|
291
|
+
def retry(self, app_id: str, **input: Any) -> Any:
|
|
292
|
+
"""Retry courseware conversion. POST /v1/coursewares/retry."""
|
|
293
|
+
return self._transport._request("POST", "/v1/coursewares/retry", "app", input, app_id, None)
|
|
294
|
+
|
|
295
|
+
class ConsoleLiveVideosApi:
|
|
296
|
+
def __init__(self, transport):
|
|
297
|
+
self._transport = transport
|
|
298
|
+
|
|
299
|
+
def list(self, app_id: str, **input: Any) -> Any:
|
|
300
|
+
"""List live video assets. GET /v1/live-videos."""
|
|
301
|
+
return self._transport._request("GET", "/v1/live-videos", "app", input, app_id, None)
|
|
302
|
+
|
|
303
|
+
def create_upload_url(self, app_id: str, **input: Any) -> Any:
|
|
304
|
+
"""Create live video upload URL. POST /v1/live-videos/upload-url."""
|
|
305
|
+
return self._transport._request("POST", "/v1/live-videos/upload-url", "app", input, app_id, None)
|
|
306
|
+
|
|
307
|
+
def create(self, app_id: str, **input: Any) -> Any:
|
|
308
|
+
"""Create live video asset. POST /v1/live-videos."""
|
|
309
|
+
return self._transport._request("POST", "/v1/live-videos", "app", input, app_id, None)
|
|
310
|
+
|
|
311
|
+
def remove(self, app_id: str, **input: Any) -> Any:
|
|
312
|
+
"""Delete live video asset. DELETE /v1/live-videos/remove."""
|
|
313
|
+
return self._transport._request("DELETE", "/v1/live-videos/remove", "app", input, app_id, None)
|
|
314
|
+
|
|
315
|
+
class ConsoleDashboardApi:
|
|
316
|
+
def __init__(self, transport):
|
|
317
|
+
self._transport = transport
|
|
318
|
+
|
|
319
|
+
def overview(self, **input: Any) -> Any:
|
|
320
|
+
"""Overview. GET /v1/dashboard/overview."""
|
|
321
|
+
return self._transport._request("GET", "/v1/dashboard/overview", "tenant", input, None, None)
|
|
322
|
+
|
|
323
|
+
def trends(self, **input: Any) -> Any:
|
|
324
|
+
"""Trends. GET /v1/dashboard/trends."""
|
|
325
|
+
return self._transport._request("GET", "/v1/dashboard/trends", "tenant", input, None, None)
|
|
326
|
+
|
|
327
|
+
def classrooms(self, **input: Any) -> Any:
|
|
328
|
+
"""Classroom statistics. GET /v1/dashboard/classrooms."""
|
|
329
|
+
return self._transport._request("GET", "/v1/dashboard/classrooms", "tenant", input, None, None)
|
|
330
|
+
|
|
331
|
+
def quality(self, **input: Any) -> Any:
|
|
332
|
+
"""Entry quality. GET /v1/dashboard/quality."""
|
|
333
|
+
return self._transport._request("GET", "/v1/dashboard/quality", "tenant", input, None, None)
|
|
334
|
+
|
|
335
|
+
def usage(self, **input: Any) -> Any:
|
|
336
|
+
"""Usage summary. GET /v1/dashboard/usage."""
|
|
337
|
+
return self._transport._request("GET", "/v1/dashboard/usage", "tenant", input, None, None)
|
|
338
|
+
|
|
339
|
+
def issues(self, **input: Any) -> Any:
|
|
340
|
+
"""Courseware issues. GET /v1/dashboard/issues."""
|
|
341
|
+
return self._transport._request("GET", "/v1/dashboard/issues", "tenant", input, None, None)
|
|
342
|
+
|
|
343
|
+
class ConsoleBillingApi:
|
|
344
|
+
def __init__(self, transport):
|
|
345
|
+
self._transport = transport
|
|
346
|
+
|
|
347
|
+
def wallet(self, **input: Any) -> Any:
|
|
348
|
+
"""Wallet balance. GET /v1/billing/wallet."""
|
|
349
|
+
return self._transport._request("GET", "/v1/billing/wallet", "tenant", input, None, None)
|
|
350
|
+
|
|
351
|
+
def entitlements(self, **input: Any) -> Any:
|
|
352
|
+
"""Tenant entitlements. GET /v1/billing/entitlements."""
|
|
353
|
+
return self._transport._request("GET", "/v1/billing/entitlements", "tenant", input, None, None)
|
|
354
|
+
|
|
355
|
+
def list_plans(self, **input: Any) -> Any:
|
|
356
|
+
"""Plan catalog. GET /v1/billing/plans."""
|
|
357
|
+
return self._transport._request("GET", "/v1/billing/plans", "tenant", input, None, None)
|
|
358
|
+
|
|
359
|
+
def list_packages(self, **input: Any) -> Any:
|
|
360
|
+
"""Resource package catalog. GET /v1/billing/packages."""
|
|
361
|
+
return self._transport._request("GET", "/v1/billing/packages", "tenant", input, None, None)
|
|
362
|
+
|
|
363
|
+
def estimate(self, **input: Any) -> Any:
|
|
364
|
+
"""Estimate classroom usage. POST /v1/billing/estimate."""
|
|
365
|
+
return self._transport._request("POST", "/v1/billing/estimate", "tenant", input, None, None)
|
|
366
|
+
|
|
367
|
+
def list_ledger(self, **input: Any) -> Any:
|
|
368
|
+
"""Points ledger. GET /v1/billing/ledger."""
|
|
369
|
+
return self._transport._request("GET", "/v1/billing/ledger", "tenant", input, None, None)
|
|
370
|
+
|
|
371
|
+
def list_usage(self, **input: Any) -> Any:
|
|
372
|
+
"""Usage records. GET /v1/billing/usage."""
|
|
373
|
+
return self._transport._request("GET", "/v1/billing/usage", "tenant", input, None, None)
|
|
374
|
+
|
|
375
|
+
def list_reservations(self, **input: Any) -> Any:
|
|
376
|
+
"""Resource reservations. GET /v1/billing/reservations."""
|
|
377
|
+
return self._transport._request("GET", "/v1/billing/reservations", "tenant", input, None, None)
|
|
378
|
+
|
|
379
|
+
def purchase_plan(self, **input: Any) -> Any:
|
|
380
|
+
"""Purchase plan. POST /v1/billing/plans/purchase."""
|
|
381
|
+
return self._transport._request("POST", "/v1/billing/plans/purchase", "tenant", input, None, None)
|
|
382
|
+
|
|
383
|
+
def purchase_package(self, **input: Any) -> Any:
|
|
384
|
+
"""Purchase resource package. POST /v1/billing/packages/purchase."""
|
|
385
|
+
return self._transport._request("POST", "/v1/billing/packages/purchase", "tenant", input, None, None)
|
|
386
|
+
|
|
387
|
+
def get_auto_recharge(self, **input: Any) -> Any:
|
|
388
|
+
"""Get auto recharge. GET /v1/billing/auto-recharge."""
|
|
389
|
+
return self._transport._request("GET", "/v1/billing/auto-recharge", "tenant", input, None, None)
|
|
390
|
+
|
|
391
|
+
def set_auto_recharge(self, **input: Any) -> Any:
|
|
392
|
+
"""Set auto recharge. POST /v1/billing/auto-recharge."""
|
|
393
|
+
return self._transport._request("POST", "/v1/billing/auto-recharge", "tenant", input, None, None)
|
|
394
|
+
|
|
395
|
+
def create_apis(transport):
|
|
396
|
+
return {
|
|
397
|
+
"auth": ConsoleAuthApi(transport),
|
|
398
|
+
"apps": ConsoleAppsApi(transport),
|
|
399
|
+
"users": ConsoleUsersApi(transport),
|
|
400
|
+
"classrooms": ConsoleClassroomsApi(transport),
|
|
401
|
+
"entry_codes": ConsoleEntryCodesApi(transport),
|
|
402
|
+
"activity": ConsoleActivityApi(transport),
|
|
403
|
+
"coursewares": ConsoleCoursewaresApi(transport),
|
|
404
|
+
"live_videos": ConsoleLiveVideosApi(transport),
|
|
405
|
+
"dashboard": ConsoleDashboardApi(transport),
|
|
406
|
+
"billing": ConsoleBillingApi(transport),
|
|
407
|
+
}
|
|
@@ -47,7 +47,7 @@ class HttpTransport:
|
|
|
47
47
|
self.fetch = fetch or _default_fetch
|
|
48
48
|
self.last_trace_id = ""
|
|
49
49
|
|
|
50
|
-
def request(self, method: str, path: str, body: Optional[Any] = None) -> Any:
|
|
50
|
+
def request(self, method: str, path: str, body: Optional[Any] = None, extra_headers: Optional[dict[str, str]] = None) -> Any:
|
|
51
51
|
trace_id = uuid.uuid4().hex[:16]
|
|
52
52
|
payload = None if body is None else json.dumps(body).encode("utf-8")
|
|
53
53
|
headers = {
|
|
@@ -58,6 +58,10 @@ class HttpTransport:
|
|
|
58
58
|
"x-lang": self.lang,
|
|
59
59
|
"x-trace-id": trace_id,
|
|
60
60
|
}
|
|
61
|
+
if self.source == "console":
|
|
62
|
+
headers.pop("x-app-key")
|
|
63
|
+
headers.pop("x-app-secret")
|
|
64
|
+
headers.update(extra_headers or {})
|
|
61
65
|
try:
|
|
62
66
|
status, response_headers, raw = self.fetch(
|
|
63
67
|
f"{self.base_url}{path}",
|
|
@@ -21,7 +21,7 @@ class _Auth:
|
|
|
21
21
|
expires_in = input.get("expiresIn", 3600)
|
|
22
22
|
token, expires_at = sign_hs256(self._app_id, self._app_secret, user_id,
|
|
23
23
|
"eduskit", "eduskit-room", expires_in,
|
|
24
|
-
{"app_id": self._app_id, "room_id": room_id, "role": role, "source": "server_sdk"},
|
|
24
|
+
{"app_id": self._app_id, "room_id": room_id, "role": role, "source": "server_sdk", "access_generation": None},
|
|
25
25
|
"whiteboard")
|
|
26
26
|
return {"token": token, "appId": self._app_id, "roomId": room_id, "userId": user_id,
|
|
27
27
|
"role": role, "expiresIn": expires_in, "expiresAt": iso_time(expires_at)}
|
|
@@ -93,12 +93,54 @@ class _Files:
|
|
|
93
93
|
return self._http.request("GET", '/v1/files/convert' + "?" + urlencode({'jobId': job_id}))
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
class _Rooms:
|
|
97
|
+
def __init__(self, http: HttpTransport) -> None:
|
|
98
|
+
self._http = http
|
|
99
|
+
|
|
100
|
+
def schedule_private_room_writes(self, room_id: str, request_id: str, opens_at: str, closes_at: str) -> Any:
|
|
101
|
+
return self._http.request("POST", "/v1/rooms/private/write-window", {"roomId": room_id, "requestId": request_id, "opensAt": opens_at, "closesAt": closes_at})
|
|
102
|
+
|
|
103
|
+
def provision_private_room(self, room_id: str, assignment_id: str) -> Any:
|
|
104
|
+
return self._http.request("POST", "/v1/rooms/private", {"roomId": room_id, "assignmentId": assignment_id})
|
|
105
|
+
|
|
106
|
+
def initialize_private_workspace(self, room_id: str, assignment_id: str, source_snapshot_id: str | None) -> Any:
|
|
107
|
+
return self._http.request("POST", "/v1/rooms/private/initializations", {"roomId": room_id, "assignmentId": assignment_id, "sourceSnapshotId": source_snapshot_id})
|
|
108
|
+
|
|
109
|
+
def get_private_workspace_initialization(self, room_id: str) -> Any:
|
|
110
|
+
return self._http.request("POST", "/v1/rooms/private/initializations/query", {"roomId": room_id})
|
|
111
|
+
|
|
112
|
+
def change_private_room_grant(self, room_id: str, **input: Any) -> Any:
|
|
113
|
+
return self._http.request("POST", "/v1/rooms/private/grants", {**input, "roomId": room_id})
|
|
114
|
+
|
|
115
|
+
def get_private_room_access(self, room_id: str, user_id: str) -> Any:
|
|
116
|
+
return self._http.request("POST", "/v1/rooms/private/access/query", {"roomId": room_id, "userId": user_id})
|
|
117
|
+
|
|
118
|
+
def issue_private_room_token(self, room_id: str, **input: Any) -> Any:
|
|
119
|
+
return self._http.request("POST", "/v1/rooms/private/token", {**input, "roomId": room_id})
|
|
120
|
+
|
|
121
|
+
def seal_private_room(self, room_id: str) -> Any:
|
|
122
|
+
return self._http.request("POST", "/v1/rooms/private/seal", {"roomId": room_id})
|
|
123
|
+
|
|
124
|
+
def create_frozen_snapshot(self, room_id: str, snapshot_id: str) -> Any:
|
|
125
|
+
return self._http.request("POST", "/v1/rooms/private/snapshots", {"roomId": room_id, "snapshotId": snapshot_id})
|
|
126
|
+
|
|
127
|
+
def get_frozen_snapshot(self, room_id: str, snapshot_id: str) -> Any:
|
|
128
|
+
return self._http.request("POST", "/v1/rooms/private/snapshots/query", {"roomId": room_id, "snapshotId": snapshot_id})
|
|
129
|
+
|
|
130
|
+
def get_frozen_snapshot_download(self, room_id: str, snapshot_id: str) -> Any:
|
|
131
|
+
return self._http.request("POST", "/v1/rooms/private/snapshots/download", {"roomId": room_id, "snapshotId": snapshot_id})
|
|
132
|
+
|
|
133
|
+
def attach_courseware(self, room_id: str, **input: Any) -> Any:
|
|
134
|
+
return self._http.request("POST", "/v1/rooms/coursewares", {**input, "roomId": room_id})
|
|
135
|
+
|
|
136
|
+
|
|
96
137
|
class WhiteboardClient:
|
|
97
138
|
def __init__(self, http: HttpTransport, *, app_id: str, app_secret: str) -> None:
|
|
98
139
|
self.auth = _Auth(app_id, app_secret)
|
|
99
140
|
self.recordings = _Recordings(http)
|
|
100
141
|
self.captures = _Captures(http)
|
|
101
142
|
self.files = _Files(http)
|
|
143
|
+
self.rooms = _Rooms(http)
|
|
102
144
|
self._http = http
|
|
103
145
|
|
|
104
146
|
@property
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eduskit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Eduskit B-end server SDK for classroom and whiteboard APIs
|
|
5
5
|
Author: Eduskit
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -68,7 +68,7 @@ token = sdk.client.auth.issue_token(eduUserId=user["eduUserId"], originId="stu_0
|
|
|
68
68
|
classroom = sdk.client.classrooms.create(
|
|
69
69
|
name="一年级数学",
|
|
70
70
|
startsAt="2026-08-17T10:00:00.000Z",
|
|
71
|
-
|
|
71
|
+
durationMinutes=60,
|
|
72
72
|
teacherEduUserId=user["eduUserId"],
|
|
73
73
|
)
|
|
74
74
|
sdk.client.classrooms.start(classroom["classroomId"])
|
|
@@ -144,3 +144,25 @@ except EduskitError as e:
|
|
|
144
144
|
## 示例
|
|
145
145
|
|
|
146
146
|
- [examples/classroom_quickstart.py](examples/classroom_quickstart.py)
|
|
147
|
+
|
|
148
|
+
## Application business APIs
|
|
149
|
+
|
|
150
|
+
Use the application-credential `client` for user lists and status, classroom queries and updates, courseware libraries, live assets, attendance, entry diagnostics, and activity logs. No console tenant token is required. See [the complete API reference](https://docs.eduskit.com/server-sdk/business-api/). New methods require a matching server-api deployment and a source SDK build.
|
|
151
|
+
|
|
152
|
+
## Console management
|
|
153
|
+
|
|
154
|
+
`ConsoleClient` exposes tenant management, authentication, applications, users, classrooms, courseware, live video, diagnostics, dashboards, and billing. Use the console-api URL and a tenant access token. Application-scoped methods require `appId`; app credentials are not sent by this client. This API is available in this source build.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import os
|
|
158
|
+
from eduskit import Eduskit
|
|
159
|
+
sdk = Eduskit(console_client={
|
|
160
|
+
"base_url": os.environ["EDU_CONSOLE_URL"],
|
|
161
|
+
"access_token": os.environ["EDU_TENANT_ACCESS_TOKEN"],
|
|
162
|
+
})
|
|
163
|
+
c = sdk.console_client
|
|
164
|
+
apps = c.apps.list()
|
|
165
|
+
users = c.users.list(os.environ["EDU_APP_ID"], page=1, pageSize=20)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Full API fields and workflows: [Console management](https://docs.eduskit.com/server-sdk/console/).
|
|
@@ -3,6 +3,10 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
eduskit/__init__.py
|
|
5
5
|
eduskit/classroom.py
|
|
6
|
+
eduskit/classroom_media.py
|
|
7
|
+
eduskit/console.py
|
|
8
|
+
eduskit/console.pyi
|
|
9
|
+
eduskit/console_api.py
|
|
6
10
|
eduskit/errors.py
|
|
7
11
|
eduskit/http.py
|
|
8
12
|
eduskit/token.py
|
|
@@ -11,4 +15,5 @@ eduskit.egg-info/PKG-INFO
|
|
|
11
15
|
eduskit.egg-info/SOURCES.txt
|
|
12
16
|
eduskit.egg-info/dependency_links.txt
|
|
13
17
|
eduskit.egg-info/top_level.txt
|
|
18
|
+
tests/test_console.py
|
|
14
19
|
tests/test_eduskit.py
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import urlsplit, parse_qs
|
|
6
|
+
from eduskit import ConsoleClient, Eduskit, EduskitError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def snake(name):
|
|
10
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConsoleTests(unittest.TestCase):
|
|
14
|
+
def test_all_operations(self):
|
|
15
|
+
ops = json.loads(Path(__file__).with_name('console-contract.json').read_text())
|
|
16
|
+
active = None
|
|
17
|
+
count = 0
|
|
18
|
+
def fetch(url, method, headers, body, timeout):
|
|
19
|
+
nonlocal count
|
|
20
|
+
count += 1
|
|
21
|
+
route = urlsplit(url)
|
|
22
|
+
self.assertEqual(route.path, active['path'])
|
|
23
|
+
self.assertEqual(method, active['method'])
|
|
24
|
+
self.assertEqual(headers.get('Authorization'), None if active['scope'] == 'public' else 'Bearer token')
|
|
25
|
+
self.assertEqual(headers.get('x-app-id'), 'app_测试' if active['scope'] == 'app' else None)
|
|
26
|
+
self.assertNotIn('x-app-key', headers)
|
|
27
|
+
self.assertNotIn('x-app-secret', headers)
|
|
28
|
+
if active['input'] != 'EmptyInput':
|
|
29
|
+
if active.get('queryKeys'):
|
|
30
|
+
self.assertEqual(parse_qs(route.query)['classroomId'], ['cls_中 &/?'])
|
|
31
|
+
self.assertNotIn('classroomId', json.loads(body))
|
|
32
|
+
elif method in ('GET', 'DELETE'):
|
|
33
|
+
query = parse_qs(route.query)
|
|
34
|
+
self.assertEqual(query['q'], ['中 &/?'])
|
|
35
|
+
self.assertEqual(query['enabled'], ['false'])
|
|
36
|
+
self.assertEqual(query['page'], ['0'])
|
|
37
|
+
self.assertIsNone(body)
|
|
38
|
+
else:
|
|
39
|
+
self.assertEqual(json.loads(body)['classroomId'], 'cls_中 &/?')
|
|
40
|
+
return 200, {}, b'{"code":0,"data":[{"ok":true}],"traceId":"trace"}'
|
|
41
|
+
c = ConsoleClient(base_url='https://console.test', access_token='token', fetch=fetch)
|
|
42
|
+
for active in ops:
|
|
43
|
+
group, method = map(snake, active['name'].split('.'))
|
|
44
|
+
args = ['app_测试'] if active['scope'] == 'app' else []
|
|
45
|
+
payload = {} if active['input'] == 'EmptyInput' else dict(classroomId='cls_中 &/?', q='中 &/?', enabled=False, page=0, unused=None)
|
|
46
|
+
self.assertEqual(getattr(getattr(c, group), method)(*args, **payload), [{'ok': True}])
|
|
47
|
+
self.assertEqual(c.last_trace_id, 'trace')
|
|
48
|
+
self.assertEqual(count, len(ops))
|
|
49
|
+
|
|
50
|
+
def test_guards_errors_and_upload(self):
|
|
51
|
+
calls = []
|
|
52
|
+
def fetch(url, method, headers, body, timeout):
|
|
53
|
+
calls.append(url)
|
|
54
|
+
if 'storage.test' in url:
|
|
55
|
+
self.assertEqual(headers, {'Content-Type': 'application/octet-stream'})
|
|
56
|
+
self.assertEqual(body, b'\x00\xff\x01')
|
|
57
|
+
return 200, {}, b''
|
|
58
|
+
self.assertEqual(headers['Authorization'], 'Bearer updated')
|
|
59
|
+
return 403, {}, b'{"code":403,"errorCode":"APP_NOT_OWNED","traceId":"trace"}'
|
|
60
|
+
c = ConsoleClient(base_url='https://console.test', fetch=fetch)
|
|
61
|
+
with self.assertRaises(EduskitError) as error: c.apps.list()
|
|
62
|
+
self.assertEqual(error.exception.error_code, 'SDK_CONSOLE_TOKEN_REQUIRED')
|
|
63
|
+
c.set_access_token('updated')
|
|
64
|
+
with self.assertRaises(EduskitError): c.users.list(' ')
|
|
65
|
+
self.assertEqual(calls, [])
|
|
66
|
+
with self.assertRaises(EduskitError) as error: c.apps.list()
|
|
67
|
+
self.assertEqual(error.exception.source, 'console')
|
|
68
|
+
self.assertEqual(error.exception.trace_id, 'trace')
|
|
69
|
+
c.upload_file('https://storage.test/signed', b'\x00\xff\x01', 'application/octet-stream')
|
|
70
|
+
with self.assertRaises(EduskitError): Eduskit().console_client
|
|
71
|
+
|
|
72
|
+
def test_app_media(self):
|
|
73
|
+
active = None
|
|
74
|
+
def fetch(url, method, headers, body, timeout):
|
|
75
|
+
self.assertEqual(urlsplit(url).path, active['path'])
|
|
76
|
+
self.assertEqual(method, active['method'])
|
|
77
|
+
self.assertEqual(headers['x-app-key'], 'key')
|
|
78
|
+
self.assertEqual(headers['x-app-secret'], 'secret')
|
|
79
|
+
self.assertNotIn('Authorization', headers)
|
|
80
|
+
if method in ('GET', 'DELETE'):
|
|
81
|
+
self.assertEqual(parse_qs(urlsplit(url).query)['classroomId'], ['cls_中 &/?'])
|
|
82
|
+
else: self.assertEqual(json.loads(body)['classroomId'], 'cls_中 &/?')
|
|
83
|
+
return 200, {}, b'{"code":0,"data":{"ok":true}}'
|
|
84
|
+
c = Eduskit(client=dict(base_url='https://server.test', app_id='app_1', app_key='key', app_secret='secret'), fetch=fetch).client
|
|
85
|
+
for active in json.loads(Path(__file__).with_name('media-contract.json').read_text()):
|
|
86
|
+
group, method = map(snake, active['name'].split('.'))
|
|
87
|
+
self.assertEqual(getattr(getattr(c, group), method)(classroomId='cls_中 &/?'), {'ok': True})
|
|
88
|
+
|
|
89
|
+
def test_business_upload_and_false_query(self):
|
|
90
|
+
def fetch(url, method, headers, body, timeout):
|
|
91
|
+
if 'storage.test' in url:
|
|
92
|
+
self.assertEqual(method, 'PUT')
|
|
93
|
+
self.assertEqual(headers, {'Content-Type': 'application/pdf'})
|
|
94
|
+
self.assertEqual(body, b'\x00\xff\x01')
|
|
95
|
+
return 200, {}, b''
|
|
96
|
+
self.assertEqual(parse_qs(urlsplit(url).query)['requireOriginId'], ['false'])
|
|
97
|
+
self.assertNotIn('Authorization', headers)
|
|
98
|
+
return 200, {}, b'{"code":0,"data":{"items":[]}}'
|
|
99
|
+
c = Eduskit(client=dict(base_url='https://server.test', app_id='app_1', app_key='key', app_secret='secret'), fetch=fetch).client
|
|
100
|
+
c.upload_file('https://storage.test/signed', b'\x00\xff\x01', 'application/pdf')
|
|
101
|
+
c.users.search(requireOriginId=False)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import base64
|
|
2
3
|
import unittest
|
|
3
4
|
|
|
4
5
|
from eduskit import Eduskit, EduskitError
|
|
@@ -22,6 +23,39 @@ def mock_fetch(body, status=200, headers=None):
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class EduskitTest(unittest.TestCase):
|
|
26
|
+
def test_private_room_server_routes(self):
|
|
27
|
+
fetch, calls = mock_fetch({"code": 0, "data": {"marker": "server-result"}})
|
|
28
|
+
sdk = Eduskit(whiteboard_client={"base_url": "http://wb.test", "app_id": "app_wb", "app_key": "wk", "app_secret": "ws"}, fetch=fetch)
|
|
29
|
+
rooms = sdk.whiteboard_client.rooms
|
|
30
|
+
operations = [
|
|
31
|
+
lambda: rooms.provision_private_room("room_a", "assignment_a"),
|
|
32
|
+
lambda: rooms.change_private_room_grant("room_a", userId="student_a", requestId="grant_a", expectedGeneration="9223372036854775806", action="grant", role="participant"),
|
|
33
|
+
lambda: rooms.get_private_room_access("room_a", "student_a"),
|
|
34
|
+
lambda: rooms.issue_private_room_token("room_a", userId="student_a", role="participant", expiresIn=600),
|
|
35
|
+
lambda: rooms.seal_private_room("room_a"),
|
|
36
|
+
lambda: rooms.create_frozen_snapshot("room_a", "snapshot_a"),
|
|
37
|
+
lambda: rooms.get_frozen_snapshot("room_a", "snapshot_a"),
|
|
38
|
+
lambda: rooms.get_frozen_snapshot_download("room_a", "snapshot_a"),
|
|
39
|
+
lambda: rooms.initialize_private_workspace("room_a", "assignment_a", None),
|
|
40
|
+
lambda: rooms.initialize_private_workspace("room_a", "assignment_a", "snapshot_a"),
|
|
41
|
+
lambda: rooms.get_private_workspace_initialization("room_a"),
|
|
42
|
+
lambda: rooms.schedule_private_room_writes("room_a", "window_a", "2026-10-02T00:00:00.000Z", "2026-10-02T00:10:00.000Z"),
|
|
43
|
+
]
|
|
44
|
+
for operation in operations:
|
|
45
|
+
self.assertEqual(operation(), {"marker": "server-result"})
|
|
46
|
+
expected = ["", "/grants", "/access/query", "/token", "/seal", "/snapshots", "/snapshots/query", "/snapshots/download", "/initializations", "/initializations", "/initializations/query", "/write-window"]
|
|
47
|
+
self.assertEqual([call["url"] for call in calls], ["http://wb.test/v1/rooms/private" + suffix for suffix in expected])
|
|
48
|
+
for call in calls:
|
|
49
|
+
self.assertEqual(call["method"], "POST")
|
|
50
|
+
self.assertEqual(call["body"]["roomId"], "room_a")
|
|
51
|
+
self.assertEqual(call["headers"]["x-app-key"], "wk")
|
|
52
|
+
self.assertEqual(calls[1]["body"]["expectedGeneration"], "9223372036854775806")
|
|
53
|
+
self.assertNotIn("accessGeneration", calls[3]["body"])
|
|
54
|
+
self.assertEqual(calls[8]["body"], {"roomId": "room_a", "assignmentId": "assignment_a", "sourceSnapshotId": None})
|
|
55
|
+
self.assertEqual(calls[9]["body"]["sourceSnapshotId"], "snapshot_a")
|
|
56
|
+
self.assertEqual(calls[10]["body"], {"roomId": "room_a"})
|
|
57
|
+
self.assertEqual(calls[11]["body"], {"roomId":"room_a", "requestId":"window_a", "opensAt":"2026-10-02T00:00:00.000Z", "closesAt":"2026-10-02T00:10:00.000Z"})
|
|
58
|
+
|
|
25
59
|
def test_classroom_register(self):
|
|
26
60
|
fetch, calls = mock_fetch(
|
|
27
61
|
{
|
|
@@ -56,6 +90,10 @@ class EduskitTest(unittest.TestCase):
|
|
|
56
90
|
)
|
|
57
91
|
self.assertEqual(token["appId"], "app_wb")
|
|
58
92
|
self.assertEqual(len(token["token"].split(".")), 3)
|
|
93
|
+
segment = token["token"].split(".")[1]
|
|
94
|
+
claims = json.loads(base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4)))
|
|
95
|
+
self.assertIn("access_generation", claims)
|
|
96
|
+
self.assertIsNone(claims["access_generation"])
|
|
59
97
|
self.assertEqual(calls, [])
|
|
60
98
|
|
|
61
99
|
def test_error_envelope(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|