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.
@@ -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()