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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eduskit
3
- Version: 0.1.4
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
- endsAt="2026-08-17T11:00:00.000Z",
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
- endsAt="2026-08-17T11:00:00.000Z",
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 _Classrooms:
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.4
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
- endsAt="2026-08-17T11:00:00.000Z",
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "eduskit"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "Eduskit B-end server SDK for classroom and whiteboard APIs"
5
5
  requires-python = ">=3.9"
6
6
  readme = "README.md"
@@ -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