ed-api-client 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ed_api_client/__init__.py +9 -0
- ed_api_client/assignments.py +241 -0
- ed_api_client/canvas.py +67 -0
- ed_api_client/challenges.py +279 -0
- ed_api_client/client.py +272 -0
- ed_api_client/quizzes.py +282 -0
- ed_api_client/slides.py +233 -0
- ed_api_client/users.py +56 -0
- ed_api_client/websockets.py +104 -0
- ed_api_client/workspaces.py +530 -0
- ed_api_client-0.1.0.dist-info/METADATA +118 -0
- ed_api_client-0.1.0.dist-info/RECORD +13 -0
- ed_api_client-0.1.0.dist-info/WHEEL +4 -0
ed_api_client/slides.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import datetime
|
|
3
|
+
from dateutil import parser
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
import operator
|
|
6
|
+
|
|
7
|
+
from ed_api_client.users import User
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Slide:
|
|
11
|
+
"""
|
|
12
|
+
A single slide within an Ed lesson
|
|
13
|
+
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
id : int
|
|
19
|
+
Unique id for the slide.
|
|
20
|
+
lessonId : int
|
|
21
|
+
The id of the lesson this slide belongs to
|
|
22
|
+
challengeId : int
|
|
23
|
+
If this slide represents a challenge that students can complete, then this will be a unique id useful for retrieving challenge submissions. Otherwise None.
|
|
24
|
+
title : str
|
|
25
|
+
The title of the slide
|
|
26
|
+
type : str
|
|
27
|
+
E.g. code, document, video, etc.
|
|
28
|
+
hidden : bool
|
|
29
|
+
True if slide is currently hidden from students, otherwise false
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
id: int
|
|
34
|
+
lessonId: int
|
|
35
|
+
challengeId: int
|
|
36
|
+
title: str
|
|
37
|
+
type: str
|
|
38
|
+
hidden: bool
|
|
39
|
+
|
|
40
|
+
def from_json(s: dict):
|
|
41
|
+
return Slide(
|
|
42
|
+
s["id"],
|
|
43
|
+
s["lesson_id"],
|
|
44
|
+
s.get("challenge_id"),
|
|
45
|
+
s["title"],
|
|
46
|
+
s["type"],
|
|
47
|
+
s["is_hidden"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Lesson:
|
|
53
|
+
"""
|
|
54
|
+
A lesson within a module
|
|
55
|
+
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
Attributes
|
|
59
|
+
----------
|
|
60
|
+
id : int
|
|
61
|
+
Unique id for each lesson
|
|
62
|
+
title : str
|
|
63
|
+
The title of the lesson
|
|
64
|
+
index : int
|
|
65
|
+
For determining the order of the lesson within the module
|
|
66
|
+
slides : List[Slide]
|
|
67
|
+
A list of slides contained within the lesson
|
|
68
|
+
|
|
69
|
+
Methods
|
|
70
|
+
-------
|
|
71
|
+
add_slide(slide : Slide)
|
|
72
|
+
Adds a single slide to the lesson
|
|
73
|
+
get_slide(slideId: int)
|
|
74
|
+
Retrieves a slide with the given id
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
id: int
|
|
78
|
+
title: str
|
|
79
|
+
index: int
|
|
80
|
+
slides: List[Slide] = field(default_factory=list)
|
|
81
|
+
_slidesById: Dict[int, Slide] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
def from_json(j: dict):
|
|
84
|
+
return Lesson(j["id"], j["title"], j["index"])
|
|
85
|
+
|
|
86
|
+
def add_slide(self, slide: Slide):
|
|
87
|
+
if slide.id not in self._slidesById:
|
|
88
|
+
self._slidesById[slide.id] = slide
|
|
89
|
+
self.slides.append(slide)
|
|
90
|
+
|
|
91
|
+
def get_slide(self, id: int) -> Slide:
|
|
92
|
+
return self._slidesById.get(id)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Module:
|
|
97
|
+
"""
|
|
98
|
+
A module within the Ed lessons page
|
|
99
|
+
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
Attributes
|
|
103
|
+
----------
|
|
104
|
+
id : int
|
|
105
|
+
Unique id for each module
|
|
106
|
+
name : str
|
|
107
|
+
The name of the module
|
|
108
|
+
index : int
|
|
109
|
+
For determining the order of the module in the lessons page
|
|
110
|
+
lessons : List[Lesson]
|
|
111
|
+
A list of lessons contained within the module
|
|
112
|
+
|
|
113
|
+
Methods
|
|
114
|
+
-------
|
|
115
|
+
add_lesson(lesson : Lesson)
|
|
116
|
+
Adds a single lesson to the module
|
|
117
|
+
get_lesson(lessonId: int)
|
|
118
|
+
Retrieves a lesson with the given id
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
id: int
|
|
122
|
+
name: str
|
|
123
|
+
index: int
|
|
124
|
+
lessons: List[Lesson] = field(default_factory=list)
|
|
125
|
+
_lessonsById: Dict[int, Lesson] = field(default_factory=dict)
|
|
126
|
+
|
|
127
|
+
def add_lesson(self, lesson: Lesson):
|
|
128
|
+
if lesson.id not in self._lessonsById:
|
|
129
|
+
self._lessonsById[lesson.id] = lesson
|
|
130
|
+
self.lessons.append(lesson)
|
|
131
|
+
self.lessons.sort(key=operator.attrgetter("index"))
|
|
132
|
+
|
|
133
|
+
def get_lesson(self, id: int) -> Lesson:
|
|
134
|
+
return self._lessonsById.get(id)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class SlideResult:
|
|
139
|
+
"""
|
|
140
|
+
Records whether a particular user has viewed, attempted or completed a particular slide
|
|
141
|
+
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
Attributes
|
|
145
|
+
----------
|
|
146
|
+
viewed : bool
|
|
147
|
+
True if the user has viewed the slide, otherwise false
|
|
148
|
+
attempted : bool
|
|
149
|
+
True if the user has attempted the slide, otherwise false
|
|
150
|
+
completed : bool
|
|
151
|
+
True if the user has completed the slide, otherwise false
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
viewed: bool
|
|
156
|
+
attempted: bool
|
|
157
|
+
completed: bool
|
|
158
|
+
|
|
159
|
+
def from_json(j: dict):
|
|
160
|
+
return SlideResult(
|
|
161
|
+
j.get("viewed", False),
|
|
162
|
+
j.get("attempted", False),
|
|
163
|
+
j.get("completed", False),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class SlideResultSet:
|
|
169
|
+
"""
|
|
170
|
+
Records student interactions with slides
|
|
171
|
+
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
Methods
|
|
175
|
+
----------
|
|
176
|
+
add (userId:int, slideId:int, result:SlideResult)
|
|
177
|
+
Adds a record of a particular user interacting with a particular slide
|
|
178
|
+
add_all (slideResults:SlideResultSet)
|
|
179
|
+
Adds all records of users interacting with slides from the given result set
|
|
180
|
+
get (userId:int, slideId:int) -> SlideResult
|
|
181
|
+
Returns the record of a particular user interacting with a particular slide
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
_resultsByUserAndSlide: Dict[int, Dict[int, SlideResult]] = field(
|
|
186
|
+
default_factory=dict
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def add(self, userId: int, slideId: int, result: SlideResult):
|
|
190
|
+
if not userId in self._resultsByUserAndSlide:
|
|
191
|
+
self._resultsByUserAndSlide[userId] = {}
|
|
192
|
+
|
|
193
|
+
self._resultsByUserAndSlide[userId][slideId] = result
|
|
194
|
+
|
|
195
|
+
def add_all(self, slideResults):
|
|
196
|
+
for userId, userResults in slideResults._resultsByUserAndSlide.items():
|
|
197
|
+
for slideId, slideResult in userResults.items():
|
|
198
|
+
self.add(userId, slideId, slideResult)
|
|
199
|
+
|
|
200
|
+
def get(self, userId: int, slideId: int):
|
|
201
|
+
return self._resultsByUserAndSlide.get(userId, {}).get(slideId)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class Override:
|
|
206
|
+
"""
|
|
207
|
+
Records whether a particular user has viewed, attempted or completed a particular slide
|
|
208
|
+
|
|
209
|
+
...
|
|
210
|
+
|
|
211
|
+
Attributes
|
|
212
|
+
----------
|
|
213
|
+
viewed : bool
|
|
214
|
+
True if the user has viewed the slide, otherwise false
|
|
215
|
+
attempted : bool
|
|
216
|
+
True if the user has attempted the slide, otherwise false
|
|
217
|
+
completed : bool
|
|
218
|
+
True if the user has completed the slide, otherwise false
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
id: int
|
|
222
|
+
lessonId: int
|
|
223
|
+
user: User
|
|
224
|
+
dueAt: datetime.datetime
|
|
225
|
+
|
|
226
|
+
def from_json(j: dict):
|
|
227
|
+
|
|
228
|
+
return Override(
|
|
229
|
+
j.get("id"),
|
|
230
|
+
j.get("lesson_id"),
|
|
231
|
+
User.from_json(j.get("user")),
|
|
232
|
+
parser.parse(j.get("due_at_override"))
|
|
233
|
+
)
|
ed_api_client/users.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class User:
|
|
7
|
+
"""
|
|
8
|
+
An Ed user account
|
|
9
|
+
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
Attributes
|
|
13
|
+
----------
|
|
14
|
+
id : int
|
|
15
|
+
Unique id for each user. This is unique to Ed and has no overlap with student ids from Canvas or CIS.
|
|
16
|
+
name : str
|
|
17
|
+
The full name of the user
|
|
18
|
+
email : str
|
|
19
|
+
The email address of the user
|
|
20
|
+
role : str
|
|
21
|
+
One of student, staff or admin
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
id: int
|
|
25
|
+
name: str
|
|
26
|
+
email: str
|
|
27
|
+
role: str
|
|
28
|
+
|
|
29
|
+
def from_json(j: dict):
|
|
30
|
+
return User(j["id"], j["name"], j["email"], j["role"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Users:
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.users = []
|
|
36
|
+
self._usersById = {}
|
|
37
|
+
self._usersByEmail = {}
|
|
38
|
+
|
|
39
|
+
def add(self, user: User):
|
|
40
|
+
self.users.append(user)
|
|
41
|
+
self._usersById[user.id] = user
|
|
42
|
+
self._usersByEmail[user.email] = user
|
|
43
|
+
|
|
44
|
+
def getById(self, id: int):
|
|
45
|
+
return self._usersById.get(id)
|
|
46
|
+
|
|
47
|
+
def getByEmail(self, email: str):
|
|
48
|
+
return self._usersByEmail.get(email)
|
|
49
|
+
|
|
50
|
+
def getAllByEmail(self, emails: List[str]):
|
|
51
|
+
users = []
|
|
52
|
+
for email in emails:
|
|
53
|
+
if email in self._usersByEmail:
|
|
54
|
+
users.append(self.getByEmail(email))
|
|
55
|
+
|
|
56
|
+
return users
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import websocket
|
|
2
|
+
import json
|
|
3
|
+
import datetime
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class File:
|
|
9
|
+
id: int
|
|
10
|
+
path: str
|
|
11
|
+
content: str
|
|
12
|
+
|
|
13
|
+
def from_json(json: dict):
|
|
14
|
+
return File(
|
|
15
|
+
json.get("data", {}).get("fid"),
|
|
16
|
+
json.get("data", {}).get("path"),
|
|
17
|
+
json.get("data", {}).get("buffer"),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ScaffoldSession:
|
|
22
|
+
def __init__(self, ticket):
|
|
23
|
+
|
|
24
|
+
self.host = "wss://sahara.au.edstem.org"
|
|
25
|
+
self.ticket = ticket
|
|
26
|
+
|
|
27
|
+
self.filesById = {}
|
|
28
|
+
|
|
29
|
+
url = f"{self.host}/connect?ticket={self.ticket}"
|
|
30
|
+
self.socket = websocket.WebSocketApp(
|
|
31
|
+
url,
|
|
32
|
+
on_message=lambda ws, msg: self.on_message(ws, msg),
|
|
33
|
+
on_error=lambda ws, error: self.on_error(ws, error),
|
|
34
|
+
on_close=lambda ws, status, msg: self.on_close(ws, status, msg),
|
|
35
|
+
on_open=lambda ws: self.on_open(ws),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def send_msg(self, msg):
|
|
39
|
+
self.socket.send(json.dumps(msg))
|
|
40
|
+
|
|
41
|
+
def send_ping(self):
|
|
42
|
+
self.send_msg(
|
|
43
|
+
{
|
|
44
|
+
"type": "ping",
|
|
45
|
+
"data": {
|
|
46
|
+
"sequence": 0,
|
|
47
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def start(self):
|
|
53
|
+
self.socket.run_forever()
|
|
54
|
+
|
|
55
|
+
def has_unopened_files(self):
|
|
56
|
+
for id, file in self.filesById.items():
|
|
57
|
+
if file is None:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def on_message(self, socket, data):
|
|
63
|
+
msg = json.loads(data)
|
|
64
|
+
# print(f"REPLAY: {msg}")
|
|
65
|
+
|
|
66
|
+
msgType = msg.get("type")
|
|
67
|
+
|
|
68
|
+
if msgType == "init":
|
|
69
|
+
|
|
70
|
+
for file in msg.get("data", {}).get("files_open", []):
|
|
71
|
+
fileId = file["id"]
|
|
72
|
+
filepath = file["path"]
|
|
73
|
+
if file["path"].endswith(".java"):
|
|
74
|
+
self.filesById[fileId] = None
|
|
75
|
+
self.send_msg(
|
|
76
|
+
{"type": "file_open", "data": {"path": filepath, "soft": False}}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if msgType == "file_ot_init":
|
|
80
|
+
|
|
81
|
+
file = File.from_json(msg)
|
|
82
|
+
self.filesById[file.id] = file
|
|
83
|
+
|
|
84
|
+
# if msgType not in ['file_ot_save', 'replay_update', 'client_leave']:
|
|
85
|
+
# self.log.write(data + "\n")
|
|
86
|
+
|
|
87
|
+
# self.send_ping()
|
|
88
|
+
|
|
89
|
+
if not self.has_unopened_files():
|
|
90
|
+
self.close()
|
|
91
|
+
|
|
92
|
+
def on_open(self, socket):
|
|
93
|
+
# print("REPLAY: opened")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
def on_error(self, socket, error):
|
|
97
|
+
print(error)
|
|
98
|
+
|
|
99
|
+
def on_close(self, socket, close_status_code, close_msg):
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
def close(self):
|
|
103
|
+
# print("closing!")
|
|
104
|
+
self.socket.close()
|