eduskit 0.1.2__tar.gz → 0.1.5__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.2
3
+ Version: 0.1.5
4
4
  Summary: Eduskit B-end server SDK for classroom and whiteboard APIs
5
5
  Author: Eduskit
6
6
  License-Expression: Apache-2.0
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from urllib.parse import urlencode
4
+
3
5
  from typing import Any
4
6
 
5
7
  from .http import HttpTransport
@@ -42,16 +44,16 @@ class _Members:
42
44
  self._http = http
43
45
 
44
46
  def add(self, classroom_id: str, **input: Any) -> Any:
45
- return self._http.request("POST", f"/v1/classrooms/{classroom_id}/members", input)
47
+ return self._http.request("POST", '/v1/classrooms/members', {**(input or {}), 'classroomId': classroom_id})
46
48
 
47
49
  def list(self, classroom_id: str) -> Any:
48
- return self._http.request("GET", f"/v1/classrooms/{classroom_id}/members")
50
+ return self._http.request("GET", '/v1/classrooms/members' + "?" + urlencode({'classroomId': classroom_id}))
49
51
 
50
52
  def replace_students(self, classroom_id: str, **input: Any) -> Any:
51
53
  return self._http.request(
52
54
  "PUT",
53
- f"/v1/classrooms/{classroom_id}/members/students",
54
- input,
55
+ '/v1/classrooms/members/students',
56
+ {**(input or {}), 'classroomId': classroom_id},
55
57
  )
56
58
 
57
59
 
@@ -62,14 +64,14 @@ class _Permissions:
62
64
  def get(self, classroom_id: str, edu_user_id: str) -> Any:
63
65
  return self._http.request(
64
66
  "GET",
65
- f"/v1/classrooms/{classroom_id}/members/{edu_user_id}/permissions",
67
+ '/v1/classrooms/members/permissions' + "?" + urlencode({'classroomId': classroom_id, 'eduUserId': edu_user_id}),
66
68
  )
67
69
 
68
70
  def set(self, classroom_id: str, edu_user_id: str, **input: Any) -> Any:
69
71
  return self._http.request(
70
72
  "POST",
71
- f"/v1/classrooms/{classroom_id}/members/{edu_user_id}/permissions",
72
- input,
73
+ '/v1/classrooms/members/permissions',
74
+ {**(input or {}), 'classroomId': classroom_id, 'eduUserId': edu_user_id},
73
75
  )
74
76
 
75
77
  def clear(
@@ -81,7 +83,7 @@ class _Permissions:
81
83
  ) -> Any:
82
84
  return self._http.request(
83
85
  "DELETE",
84
- f"/v1/classrooms/{classroom_id}/members/{edu_user_id}/permissions/{permission}",
86
+ '/v1/classrooms/members/permissions' + "?" + urlencode({'classroomId': classroom_id, 'eduUserId': edu_user_id, 'permission': permission}),
85
87
  input,
86
88
  )
87
89
 
@@ -91,19 +93,19 @@ class _Coursewares:
91
93
  self._http = http
92
94
 
93
95
  def list(self, classroom_id: str) -> Any:
94
- return self._http.request("GET", f"/v1/classrooms/{classroom_id}/coursewares")
96
+ return self._http.request("GET", '/v1/classrooms/coursewares' + "?" + urlencode({'classroomId': classroom_id}))
95
97
 
96
98
  def bind(self, classroom_id: str, **input: Any) -> Any:
97
99
  return self._http.request(
98
100
  "POST",
99
- f"/v1/classrooms/{classroom_id}/coursewares",
100
- input,
101
+ '/v1/classrooms/coursewares',
102
+ {**(input or {}), 'classroomId': classroom_id},
101
103
  )
102
104
 
103
105
  def unbind(self, classroom_id: str, **input: Any) -> Any:
104
106
  return self._http.request(
105
107
  "DELETE",
106
- f"/v1/classrooms/{classroom_id}/coursewares",
108
+ '/v1/classrooms/coursewares' + "?" + urlencode({'classroomId': classroom_id}),
107
109
  input,
108
110
  )
109
111
 
@@ -119,10 +121,10 @@ class _Classrooms:
119
121
  return self._http.request("POST", "/v1/classrooms", input)
120
122
 
121
123
  def start(self, classroom_id: str) -> Any:
122
- return self._http.request("POST", f"/v1/classrooms/{classroom_id}/start")
124
+ return self._http.request("POST", '/v1/classrooms/start', {'classroomId': classroom_id})
123
125
 
124
126
  def end(self, classroom_id: str) -> Any:
125
- return self._http.request("POST", f"/v1/classrooms/{classroom_id}/end")
127
+ return self._http.request("POST", '/v1/classrooms/end', {'classroomId': classroom_id})
126
128
 
127
129
 
128
130
  class _App:
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import urlencode
4
+
5
+ from typing import Any
6
+
7
+ from .http import HttpTransport
8
+ from .errors import EduskitError
9
+ from .token import iso_time, sign_hs256
10
+
11
+
12
+ class _Auth:
13
+ def __init__(self, app_id: str, app_secret: str) -> None:
14
+ self._app_id = app_id
15
+ self._app_secret = app_secret
16
+
17
+ def issue_room_token(self, **input: Any) -> Any:
18
+ room_id, user_id, role = input.get("roomId"), input.get("userId"), input.get("role")
19
+ if not room_id or not user_id or role not in ("host", "participant", "observer"):
20
+ raise EduskitError("roomId, userId and a valid role are required", error_code="SDK_TOKEN_INPUT_INVALID", source="whiteboard")
21
+ expires_in = input.get("expiresIn", 3600)
22
+ token, expires_at = sign_hs256(self._app_id, self._app_secret, user_id,
23
+ "eduskit", "eduskit-room", expires_in,
24
+ {"app_id": self._app_id, "room_id": room_id, "role": role, "source": "server_sdk", "access_generation": None},
25
+ "whiteboard")
26
+ return {"token": token, "appId": self._app_id, "roomId": room_id, "userId": user_id,
27
+ "role": role, "expiresIn": expires_in, "expiresAt": iso_time(expires_at)}
28
+
29
+
30
+ class _Recordings:
31
+ def __init__(self, http: HttpTransport) -> None:
32
+ self._http = http
33
+
34
+ def start(self, room_id: str, **input: Any) -> Any:
35
+ return self._http.request("POST", '/v1/rooms/recording/start', {**(input or {}), 'roomId': room_id})
36
+
37
+ def stop(self, room_id: str, **input: Any) -> Any:
38
+ return self._http.request("POST", '/v1/rooms/recording/stop', {**(input or {}), 'roomId': room_id})
39
+
40
+ def list(self, room_id: str) -> Any:
41
+ return self._http.request("GET", '/v1/rooms/recordings' + "?" + urlencode({'roomId': room_id}))
42
+
43
+ def get(self, recording_id: str) -> Any:
44
+ return self._http.request("GET", '/v1/recordings' + "?" + urlencode({'recordingId': recording_id}))
45
+
46
+ def register_media_asset(self, recording_id: str, **input: Any) -> Any:
47
+ return self._http.request(
48
+ "POST",
49
+ '/v1/recordings/media-assets',
50
+ {**(input or {}), 'recordingId': recording_id},
51
+ )
52
+
53
+ def delete_media_asset(self, recording_id: str, asset_id: str) -> Any:
54
+ return self._http.request(
55
+ "DELETE",
56
+ '/v1/recordings/media-assets' + "?" + urlencode({'recordingId': recording_id, 'assetId': asset_id}),
57
+ )
58
+
59
+ def enqueue_video_export(self, recording_id: str, **input: Any) -> Any:
60
+ return self._http.request(
61
+ "POST",
62
+ '/v1/recordings/video-exports',
63
+ {**(input or {}), 'recordingId': recording_id},
64
+ )
65
+
66
+ def get_video_export(self, recording_id: str, job_id: str) -> Any:
67
+ return self._http.request(
68
+ "GET",
69
+ '/v1/recordings/video-exports' + "?" + urlencode({'recordingId': recording_id, 'jobId': job_id}),
70
+ )
71
+
72
+
73
+ class _Captures:
74
+ def __init__(self, http: HttpTransport) -> None:
75
+ self._http = http
76
+
77
+ def create(self, room_id: str, **input: Any) -> Any:
78
+ body = {"roomId": room_id, **input}
79
+ return self._http.request("POST", '/v1/rooms/captures', {**(body or {}), 'roomId': room_id})
80
+
81
+ def list(self, room_id: str) -> Any:
82
+ return self._http.request("GET", '/v1/rooms/captures' + "?" + urlencode({'roomId': room_id}))
83
+
84
+
85
+ class _Files:
86
+ def __init__(self, http: HttpTransport) -> None:
87
+ self._http = http
88
+
89
+ def convert(self, **input: Any) -> Any:
90
+ return self._http.request("POST", "/v1/files/convert", input)
91
+
92
+ def get_convert_job(self, job_id: str) -> Any:
93
+ return self._http.request("GET", '/v1/files/convert' + "?" + urlencode({'jobId': job_id}))
94
+
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
+
137
+ class WhiteboardClient:
138
+ def __init__(self, http: HttpTransport, *, app_id: str, app_secret: str) -> None:
139
+ self.auth = _Auth(app_id, app_secret)
140
+ self.recordings = _Recordings(http)
141
+ self.captures = _Captures(http)
142
+ self.files = _Files(http)
143
+ self.rooms = _Rooms(http)
144
+ self._http = http
145
+
146
+ @property
147
+ def last_trace_id(self) -> str:
148
+ return self._http.last_trace_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eduskit
3
- Version: 0.1.2
3
+ Version: 0.1.5
4
4
  Summary: Eduskit B-end server SDK for classroom and whiteboard APIs
5
5
  Author: Eduskit
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "eduskit"
3
- version = "0.1.2"
3
+ version = "0.1.5"
4
4
  description = "Eduskit B-end server SDK for classroom and whiteboard APIs"
5
5
  requires-python = ">=3.9"
6
6
  readme = "README.md"
@@ -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):
@@ -1,104 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from .http import HttpTransport
6
- from .errors import EduskitError
7
- from .token import iso_time, sign_hs256
8
-
9
-
10
- class _Auth:
11
- def __init__(self, app_id: str, app_secret: str) -> None:
12
- self._app_id = app_id
13
- self._app_secret = app_secret
14
-
15
- def issue_room_token(self, **input: Any) -> Any:
16
- room_id, user_id, role = input.get("roomId"), input.get("userId"), input.get("role")
17
- if not room_id or not user_id or role not in ("host", "participant", "observer"):
18
- raise EduskitError("roomId, userId and a valid role are required", error_code="SDK_TOKEN_INPUT_INVALID", source="whiteboard")
19
- expires_in = input.get("expiresIn", 3600)
20
- token, expires_at = sign_hs256(self._app_id, self._app_secret, user_id,
21
- "eduskit", "eduskit-room", expires_in,
22
- {"app_id": self._app_id, "room_id": room_id, "role": role, "source": "server_sdk"},
23
- "whiteboard")
24
- return {"token": token, "appId": self._app_id, "roomId": room_id, "userId": user_id,
25
- "role": role, "expiresIn": expires_in, "expiresAt": iso_time(expires_at)}
26
-
27
-
28
- class _Recordings:
29
- def __init__(self, http: HttpTransport) -> None:
30
- self._http = http
31
-
32
- def start(self, room_id: str, **input: Any) -> Any:
33
- return self._http.request("POST", f"/v1/rooms/{room_id}/recording/start", input)
34
-
35
- def stop(self, room_id: str, **input: Any) -> Any:
36
- return self._http.request("POST", f"/v1/rooms/{room_id}/recording/stop", input)
37
-
38
- def list(self, room_id: str) -> Any:
39
- return self._http.request("GET", f"/v1/rooms/{room_id}/recordings")
40
-
41
- def get(self, recording_id: str) -> Any:
42
- return self._http.request("GET", f"/v1/recordings/{recording_id}")
43
-
44
- def register_media_asset(self, recording_id: str, **input: Any) -> Any:
45
- return self._http.request(
46
- "POST",
47
- f"/v1/recordings/{recording_id}/media-assets",
48
- input,
49
- )
50
-
51
- def delete_media_asset(self, recording_id: str, asset_id: str) -> Any:
52
- return self._http.request(
53
- "DELETE",
54
- f"/v1/recordings/{recording_id}/media-assets/{asset_id}",
55
- )
56
-
57
- def enqueue_video_export(self, recording_id: str, **input: Any) -> Any:
58
- return self._http.request(
59
- "POST",
60
- f"/v1/recordings/{recording_id}/video-exports",
61
- input,
62
- )
63
-
64
- def get_video_export(self, recording_id: str, job_id: str) -> Any:
65
- return self._http.request(
66
- "GET",
67
- f"/v1/recordings/{recording_id}/video-exports/{job_id}",
68
- )
69
-
70
-
71
- class _Captures:
72
- def __init__(self, http: HttpTransport) -> None:
73
- self._http = http
74
-
75
- def create(self, room_id: str, **input: Any) -> Any:
76
- body = {"roomId": room_id, **input}
77
- return self._http.request("POST", f"/v1/rooms/{room_id}/captures", body)
78
-
79
- def list(self, room_id: str) -> Any:
80
- return self._http.request("GET", f"/v1/rooms/{room_id}/captures")
81
-
82
-
83
- class _Files:
84
- def __init__(self, http: HttpTransport) -> None:
85
- self._http = http
86
-
87
- def convert(self, **input: Any) -> Any:
88
- return self._http.request("POST", "/v1/files/convert", input)
89
-
90
- def get_convert_job(self, job_id: str) -> Any:
91
- return self._http.request("GET", f"/v1/files/convert/{job_id}")
92
-
93
-
94
- class WhiteboardClient:
95
- def __init__(self, http: HttpTransport, *, app_id: str, app_secret: str) -> None:
96
- self.auth = _Auth(app_id, app_secret)
97
- self.recordings = _Recordings(http)
98
- self.captures = _Captures(http)
99
- self.files = _Files(http)
100
- self._http = http
101
-
102
- @property
103
- def last_trace_id(self) -> str:
104
- return self._http.last_trace_id
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes