otter-service-stdalone 1.0.0__tar.gz → 1.1.1__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.
Files changed (28) hide show
  1. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/PKG-INFO +1 -1
  2. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/setup.cfg +1 -1
  3. otter_service_stdalone-1.1.1/src/otter_service_stdalone/__init__.py +1 -0
  4. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/app.py +73 -16
  5. otter_service_stdalone-1.1.1/src/otter_service_stdalone/grade_notebooks.py +75 -0
  6. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/index.html +40 -5
  7. otter_service_stdalone-1.1.1/src/otter_service_stdalone/scripts/web_socket.js +127 -0
  8. otter_service_stdalone-1.1.1/src/otter_service_stdalone/static_files/README_DO_NOT_DISTRIBUTE.txt +9 -0
  9. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone.egg-info/PKG-INFO +1 -1
  10. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone.egg-info/SOURCES.txt +2 -0
  11. otter_service_stdalone-1.0.0/src/otter_service_stdalone/__init__.py +0 -1
  12. otter_service_stdalone-1.0.0/src/otter_service_stdalone/grade_notebooks.py +0 -105
  13. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/README.md +0 -0
  14. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/pyproject.toml +0 -0
  15. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/access_sops_keys.py +0 -0
  16. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/fs_logging.py +0 -0
  17. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/secrets/gh_key.dev.yaml +0 -0
  18. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/secrets/gh_key.local.yaml +0 -0
  19. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/secrets/gh_key.prod.yaml +0 -0
  20. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/secrets/gh_key.staging.yaml +0 -0
  21. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/static_templates/403.html +0 -0
  22. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/static_templates/500.html +0 -0
  23. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/upload_handle.py +0 -0
  24. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone/user_auth.py +0 -0
  25. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone.egg-info/dependency_links.txt +0 -0
  26. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone.egg-info/entry_points.txt +0 -0
  27. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/src/otter_service_stdalone.egg-info/top_level.txt +0 -0
  28. {otter_service_stdalone-1.0.0 → otter_service_stdalone-1.1.1}/tests/test_upload_handle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: otter_service_stdalone
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: Grading Service for Instructors using Otter Grader
5
5
  Home-page: https://github.com/sean-morris/otter-service-stdalone
6
6
  Author: Sean Morris
@@ -23,7 +23,7 @@ python_requires = >=3.8
23
23
  where = src
24
24
 
25
25
  [options.package_data]
26
- otter_service_stdalone = index.html, secrets/*.yaml, static_templates/*.html
26
+ otter_service_stdalone = index.html, secrets/*.yaml, static_templates/*.html, static_files/*, scripts/*
27
27
 
28
28
  [options.entry_points]
29
29
  console_scripts =
@@ -0,0 +1 @@
1
+ __version__ = "1.1.1"
@@ -4,10 +4,12 @@ import tornado.web
4
4
  import tornado.auth
5
5
  import os
6
6
  import uuid
7
+ import tornado.websocket
7
8
  from otter_service_stdalone import fs_logging as log
8
9
  from otter_service_stdalone import user_auth as u_auth
9
10
  from otter_service_stdalone import grade_notebooks
10
11
  from zipfile import ZipFile, ZIP_DEFLATED
12
+ import queue
11
13
 
12
14
 
13
15
  __UPLOADS__ = "/tmp/uploads"
@@ -16,6 +18,53 @@ log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
16
18
  log_http = f'{os.environ.get("ENVIRONMENT")}-http-error'
17
19
 
18
20
  authorization_states = {} # used to protect against cross-site request forgery attacks.
21
+ session_queues = {}
22
+ session_messages = {}
23
+ session_callbacks = {}
24
+
25
+
26
+ class WebSocketHandler(tornado.websocket.WebSocketHandler):
27
+ """This handles updates to the application from otter-grader
28
+ """
29
+ def open(self):
30
+ """if logged in, start a callback once a second to see if there messages for the
31
+ logged in user
32
+ """
33
+ if self.get_secure_cookie("user"):
34
+ user_id = self.get_secure_cookie("user").decode('utf-8')
35
+ if user_id not in session_callbacks:
36
+ session_callbacks[user_id] = tornado.ioloop.PeriodicCallback(lambda: self.send_results(user_id), 1000)
37
+ session_callbacks[user_id].start()
38
+
39
+ def on_message(self, message):
40
+ pass # No action needed on incoming message
41
+
42
+ def on_close(self):
43
+ """stop the periodic classback on close
44
+ """
45
+ if self.get_secure_cookie("user"):
46
+ user_id = self.get_secure_cookie("user").decode('utf-8')
47
+ if user_id in session_callbacks and session_callbacks[user_id].callback:
48
+ session_callbacks[user_id].stop()
49
+ session_callbacks.pop(user_id)
50
+
51
+ def send_results(self, user_id):
52
+ """if there are messages for this user, send them to application by submission.
53
+ We save the messages in the session so if refresh on client side they still
54
+ have the updates
55
+ """
56
+ try:
57
+ if user_id in session_queues:
58
+ user_queue_dict = session_queues[user_id]
59
+ user_messages_dict = session_messages[user_id]
60
+ if user_queue_dict:
61
+ for result_id, q in user_queue_dict.items():
62
+ if not q.empty():
63
+ while not q.empty():
64
+ user_messages_dict[result_id].append(q.get())
65
+ self.write_message({"messages": user_messages_dict})
66
+ except tornado.websocket.WebSocketClosedError:
67
+ log.write_logs("ws-error", "Web Socket Problem", "", "", log_error)
19
68
 
20
69
 
21
70
  class HealthHandler(tornado.web.RequestHandler):
@@ -136,27 +185,26 @@ class Download(BaseHandler):
136
185
  msg += "Please check the code or upload your notebooks "
137
186
  msg += "and autograder.zip for grading again."
138
187
  self.render("index.html", download_message=msg)
139
- elif not os.path.exists(f"{directory}/grading-logs.txt"):
188
+ elif not os.path.exists(f"{directory}/final_grades.csv"):
140
189
  m = "Download: Results Not Ready"
141
190
  log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
142
191
  msg = "The results of your download are not ready yet. "
143
192
  msg += "Please check back."
144
193
  self.render("index.html", download_message=msg, dcode=download_code)
145
194
  else:
146
- if not os.path.isfile(f"{directory}/final_grades.csv"):
147
- m = "Download: final_grades.csv does not exist"
148
- t = "Problem grading notebooks see stack trace"
149
- log.write_logs(download_code, m, t, "debug", log_debug)
150
- with open(f"{directory}/final_grades.csv", "a") as f:
151
- m = "There was a problem grading your notebooks. Please see grading-logs.txt"
152
- f.write(m)
153
- f.close()
154
195
  m = "Download Success: Creating results.zip"
155
196
  log.write_logs(download_code, m, "", "debug", log_debug)
156
197
  with ZipFile(f"{directory}/results.zip", 'w') as zipF:
157
- for file in ["final_grades.csv", "grading-logs.txt"]:
158
- if os.path.isfile(f"{directory}/{file}"):
159
- zipF.write(f"{directory}/{file}", file, compress_type=ZIP_DEFLATED)
198
+ final_file = "final_grades.csv"
199
+ if os.path.isfile(f"{directory}/{final_file}"):
200
+ zipF.write(f"{directory}/{final_file}", final_file, compress_type=ZIP_DEFLATED)
201
+ for filename in os.listdir(f"{directory}/grading-summaries"):
202
+ file_path = os.path.join(f"{directory}/grading-summaries", filename)
203
+ if os.path.isfile(file_path):
204
+ f_path = f"grading-summaries-do-not-distribute/{filename}"
205
+ zipF.write(f"{file_path}", f_path, compress_type=ZIP_DEFLATED)
206
+ read_me = os.path.join(os.path.dirname(__file__), "static_files", "README_DO_NOT_DISTRIBUTE.txt")
207
+ zipF.write(read_me, "README_DO_NOT_DISTRIBUTE.txt", compress_type=ZIP_DEFLATED)
160
208
 
161
209
  self.set_header('Content-Type', 'application/octet-stream')
162
210
  self.set_header("Content-Description", "File Transfer")
@@ -179,7 +227,7 @@ class Upload(BaseHandler):
179
227
 
180
228
  Args:
181
229
  tornado (tornado.web.RequestHandler): The upload request handler
182
- """
230
+ """
183
231
  @tornado.web.authenticated
184
232
  def get(self):
185
233
  # this just redirects to login and displays main page
@@ -189,6 +237,11 @@ class Upload(BaseHandler):
189
237
  async def post(self):
190
238
  """this handles the post request and asynchronously launches the grader
191
239
  """
240
+ user = self.get_current_user()
241
+ user_id = user.decode('utf-8')
242
+ if user_id not in session_queues:
243
+ session_queues[user_id] = {}
244
+ session_messages[user_id] = {}
192
245
  g = grade_notebooks.GradeNotebooks()
193
246
  files = self.request.files
194
247
  results_path = str(uuid.uuid4())
@@ -219,11 +272,13 @@ class Upload(BaseHandler):
219
272
  fh.write(notebooks['body'])
220
273
  m = "Step 3: Uploaded Files Written to Disk"
221
274
  log.write_logs(results_path, m, f"Results Code: {results_path}", "debug", log_debug)
222
- m = "Please save this code. You can retrieve your files by submitting this code "
223
- m += f"in the \"Results\" section to the right: {results_path}"
275
+ m = "Please save this code; it appears in the \"Notebook Grading Progress\" section below. You can "
276
+ m += f"retrieve your files by submitting this code in the \"Results\" section to the right: {results_path}"
224
277
  self.render("index.html", message=m)
225
278
  try:
226
- await g.grade(auto_p, notebooks_path, results_path)
279
+ session_queues[user_id][results_path] = queue.Queue()
280
+ session_messages[user_id][results_path] = []
281
+ await g.grade(auto_p, notebooks_path, results_path, session_queues[user_id].get(results_path))
227
282
  except Exception as e:
228
283
  log.write_logs(results_path, "Grading Problem", str(e), "error", log_error)
229
284
  else:
@@ -244,8 +299,10 @@ application = tornado.web.Application([
244
299
  (r"/login", LoginHandler),
245
300
  (r"/upload", Upload),
246
301
  (r"/download", Download),
302
+ (r"/update", WebSocketHandler),
247
303
  (r"/oauth_callback", GitHubOAuthHandler),
248
304
  (r"/otterhealth", HealthHandler),
305
+ (r"/scripts/(.*)", tornado.web.StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), "scripts")}),
249
306
  ], **settings, debug=False)
250
307
 
251
308
 
@@ -0,0 +1,75 @@
1
+ import asyncio
2
+ from otter_service_stdalone import fs_logging as log, upload_handle as uh
3
+ import os
4
+ from otter.grade import main as grade
5
+
6
+ log_debug = f'{os.environ.get("ENVIRONMENT")}-debug'
7
+ log_count = f'{os.environ.get("ENVIRONMENT")}-count'
8
+ log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
9
+
10
+
11
+ class GradeNotebooks():
12
+ """The class contains the async grade method for executing
13
+ otter grader as well as a function for logging the number of
14
+ notebooks to be graded
15
+ """
16
+ def count_ipynb_files(self, directory, extension):
17
+ """this count the files for logging purposes"""
18
+ count = 0
19
+ for filename in os.listdir(directory):
20
+ if filename.endswith(extension):
21
+ count += 1
22
+ return count
23
+
24
+ async def grade(self, p, notebooks_path, results_id, user_queue):
25
+ """Calls otter grade asynchronously and writes the various log files
26
+ and results of grading generating by otter-grader
27
+
28
+ Args:
29
+ p (str): the path to autograder.zip -- the solutions
30
+ notebooks_path (str): the path to the folder of notebooks to be graded
31
+ results_id (str): used for identifying logs
32
+
33
+ Raises:
34
+ Exception: Timeout Exception is raised if async takes longer than 20 min
35
+
36
+ Returns:
37
+ boolean: True is the process completes; otherwise an Exception is thrown
38
+ """
39
+ try:
40
+ notebook_folder = uh.handle_upload(notebooks_path, results_id)
41
+ notebook_count = self.count_ipynb_files(notebook_folder, ".ipynb")
42
+ log.write_logs(results_id, f"{notebook_count}",
43
+ "",
44
+ "info",
45
+ f'{os.environ.get("ENVIRONMENT")}-count')
46
+ log.write_logs(results_id, "Step 5: Notebook Folder configured and grading started",
47
+ f"Notebook Folder: {notebook_folder}",
48
+ "debug",
49
+ log_debug)
50
+
51
+ await grade(
52
+ name='grader',
53
+ autograder=p,
54
+ paths=(notebook_folder,),
55
+ containers=10,
56
+ timeout=300,
57
+ ext="ipynb",
58
+ output_dir=notebook_folder,
59
+ result_queue=user_queue
60
+ )
61
+ user_queue.put("Results available for download")
62
+
63
+ log.write_logs(results_id, "Step 6: Grading: Finished",
64
+ f"{notebook_folder}",
65
+ "debug",
66
+ log_debug)
67
+ log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
68
+ "",
69
+ "info",
70
+ log_error)
71
+ return True
72
+ except asyncio.TimeoutError:
73
+ raise Exception(f'Grading timed out for {notebook_folder}')
74
+ except Exception as e:
75
+ raise e
@@ -4,6 +4,39 @@
4
4
  <title>Upload Form</title>
5
5
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
6
6
  <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
7
+ <style>
8
+ /* Styles to make the div scrollable after a certain height */
9
+ #messages {
10
+ max-height: 400px;
11
+ overflow-y: auto; /* Enable vertical scrolling */
12
+ padding: 10px;
13
+ width: auto;
14
+ }
15
+ /* Styles for the collapsible div */
16
+ .collapsible {
17
+ border: 1px solid #ccc;
18
+ width: auto;
19
+ margin-bottom: 10px;
20
+ }
21
+
22
+ /* Styles for the header */
23
+ .header {
24
+ background-color: #f1f1f1;
25
+ padding: 10px;
26
+ cursor: pointer;
27
+ }
28
+
29
+ /* Styles for the content */
30
+ .content {
31
+ max-height: 300px;
32
+ overflow-y: auto; /* Enable vertical scrolling */
33
+ padding: 10px;
34
+ display: none;
35
+ }
36
+
37
+
38
+ </style>
39
+ <script src="/scripts/web_socket.js"></script>
7
40
  </head>
8
41
  <body>
9
42
  <section class="container">
@@ -14,7 +47,7 @@
14
47
  Cloudbank Otter Grading - Beta
15
48
  </h1>
16
49
  <h2 class="subtitle">
17
- Upload your notebooks, grade them, and download results.
50
+ Upload your notebooks, grade them, and download results.
18
51
  </h2>
19
52
  </div>
20
53
  </div>
@@ -84,15 +117,17 @@
84
117
  {% end %}
85
118
  </form>
86
119
  </div>
87
- </div>
120
+ </div>
121
+ <div id="messages">
122
+ <p class="title is-3">Notebook Grading Progress</p>
123
+ <p id="none-msg">No submissions are being graded</p>
124
+ </div>
88
125
  </div>
89
126
  </div>
90
127
  <div class="hero is-info welcome is-small">
91
128
  <div class="hero-body">
92
129
  <div class="container">
93
- <h2 class="subtitle">
94
- Questions? Problems? email: sean.smorris at berkeley.edu
95
- </h2>
130
+ <span class="has-text-right is-pulled-right">email: sean.smorris@berkeley.edu</span>
96
131
  </div>
97
132
  </div>
98
133
  </div>
@@ -0,0 +1,127 @@
1
+ /**
2
+ * This is main function of the script. It sets up the web sockets and handles messages
3
+ * coming from the application. The last line of the script calls this function
4
+ */
5
+ function connectWebSocket() {
6
+ var loc = window.location;
7
+ var wsStart = loc.protocol === "https:" ? "ws://" : "ws://";
8
+ var wsUrl = wsStart + loc.host + "/update";
9
+
10
+ var ws = new WebSocket(wsUrl);
11
+ submission_divs = {}
12
+ reconnectInterval = 3000;
13
+ ws.onopen = function() {
14
+ console.log("WebSocket is open now.");
15
+ reconnectInterval = 3000;
16
+ };
17
+
18
+ ws.onmessage = function(event) {
19
+ var messagesDiv = document.getElementById("messages");
20
+ var noneMsgP = document.getElementById("none-msg");
21
+ noneMsgP.style.display = "none"
22
+ json_msgs = JSON.parse(event.data)["messages"];
23
+ console.log(json_msgs)
24
+ Object.entries(json_msgs).forEach(([submission_key, messages], index) => {
25
+ if (!(submission_key in submission_divs)){
26
+ messagesDiv.appendChild(_setUpNewSubmission(submission_key, index, messages))
27
+ } else {
28
+ _updateSubmission(submission_key, index, messages)
29
+ }
30
+
31
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
32
+ });
33
+ };
34
+
35
+ ws.onclose = function() {
36
+ //if closes attempts re-connect every few seconds
37
+ console.log("WebSocket is closed now.");
38
+ setTimeout(connectWebSocket, 3000);
39
+ };
40
+
41
+ ws.onerror = function(error) {
42
+ console.error('WebSocket error:', error);
43
+ ws.close();
44
+ };
45
+ };
46
+
47
+ /**
48
+ * This function sets up the div that will report progress on the given submission
49
+ * in the application
50
+ * @param {*} submission_key doanload code - unique to submission
51
+ * @param {*} index - the submission number in this session for the user
52
+ * @param {*} messages - all the messages that need to be added to progress
53
+ */
54
+ function _setUpNewSubmission(submission_key, index, messages){
55
+ var newMessage = document.createElement("div");
56
+ newMessage.id = submission_key
57
+ var newMessageHeader = document.createElement("div");
58
+ var newMessageContent = document.createElement("div");
59
+ newMessageContent.id = "msg-" + submission_key
60
+ newMessageHeader.id = "header-" + submission_key
61
+ var toggleSign = document.createElement("span");
62
+ var submissionHeader = document.createElement("span");
63
+ submissionHeader.innerHTML = " Submission Progress: Submission #" + (index+1)
64
+ submissionHeader.id = "sub-header-" + submission_key
65
+ newMessage.className = "collapsible"
66
+ newMessageHeader.className = "header"
67
+ toggleSign.innerHTML = '<i class="fas fa-minus-circle"></i>';
68
+
69
+ newMessageHeader.appendChild(toggleSign)
70
+ newMessageHeader.appendChild(submissionHeader)
71
+ newMessageContent.className = "content"
72
+ newMessageContent.style.display = "block"
73
+ newMessage.appendChild(newMessageHeader)
74
+ newMessage.appendChild(newMessageContent)
75
+ submission_divs[submission_key] = newMessage
76
+ const li = document.createElement('li');
77
+ li.textContent = "Download Code: " + submission_key;
78
+ newMessageContent.appendChild(li);
79
+ for (const m of messages) {
80
+ const li = document.createElement('li');
81
+ li.textContent = m;
82
+ newMessageContent.appendChild(li);
83
+ }
84
+ // Toggle the visibility of the scrollable content
85
+ newMessageHeader.addEventListener('click', () => {
86
+ if (newMessageContent.style.display == 'none') {
87
+ newMessageContent.style.display = 'block';
88
+ toggleSign.innerHTML = '<i class="fas fa-minus-circle"></i>';
89
+ } else {
90
+ newMessageContent.style.display = 'none';
91
+ toggleSign.innerHTML = '<i class="fas fa-plus-circle"></i>';
92
+ }
93
+ });
94
+ return newMessage;
95
+ }
96
+
97
+ /**
98
+ * This updates the progress div for this submission
99
+ *
100
+ * @param {*} submission_key doanload code - unique to submission
101
+ * @param {*} index - the submission number in this session for the user
102
+ * @param {*} messages - all the messages that need to be added to progress
103
+ */
104
+ function _updateSubmission(submission_key, index, messages){
105
+ var newMessageContent = document.getElementById("msg-" + submission_key);
106
+ var items = newMessageContent.getElementsByTagName("li");
107
+ var msgs = []
108
+ for (var i = 0; i < items.length; ++i) {
109
+ msgs.push(items[i].textContent)
110
+ }
111
+ for (const m of messages) {
112
+ if(!(msgs.includes(m))){
113
+ const li = document.createElement('li');
114
+ li.textContent = m;
115
+ newMessageContent.appendChild(li);
116
+ }
117
+ }
118
+ if(messages.includes("Results available for download")){
119
+ var header = document.getElementById("header-" + submission_key)
120
+ header.style.backgroundColor = '#7BAE37';
121
+ var subheader = document.getElementById("sub-header-" + submission_key)
122
+ subheader.innerHTML = " Submission Progress: Submission #" + (index+1) + "</br>Download Code: " + submission_key;
123
+ }
124
+ }
125
+
126
+
127
+ connectWebSocket()
@@ -0,0 +1,9 @@
1
+ Please do not distribute the grade summaries in the folder, grading-summaries-do-not-distribute!
2
+
3
+ These summaries contain the answers to all questions, including the hidden tests
4
+ run by the grader.
5
+
6
+ We have included these files so you can see what errors were made by a student
7
+ on their notebook without having to re-run the notebook yourself.
8
+
9
+ Thanks!
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: otter_service_stdalone
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: Grading Service for Instructors using Otter Grader
5
5
  Home-page: https://github.com/sean-morris/otter-service-stdalone
6
6
  Author: Sean Morris
@@ -14,10 +14,12 @@ src/otter_service_stdalone.egg-info/SOURCES.txt
14
14
  src/otter_service_stdalone.egg-info/dependency_links.txt
15
15
  src/otter_service_stdalone.egg-info/entry_points.txt
16
16
  src/otter_service_stdalone.egg-info/top_level.txt
17
+ src/otter_service_stdalone/scripts/web_socket.js
17
18
  src/otter_service_stdalone/secrets/gh_key.dev.yaml
18
19
  src/otter_service_stdalone/secrets/gh_key.local.yaml
19
20
  src/otter_service_stdalone/secrets/gh_key.prod.yaml
20
21
  src/otter_service_stdalone/secrets/gh_key.staging.yaml
22
+ src/otter_service_stdalone/static_files/README_DO_NOT_DISTRIBUTE.txt
21
23
  src/otter_service_stdalone/static_templates/403.html
22
24
  src/otter_service_stdalone/static_templates/500.html
23
25
  tests/test_upload_handle.py
@@ -1 +0,0 @@
1
- __version__ = "1.0.0"
@@ -1,105 +0,0 @@
1
- import asyncio
2
- import async_timeout
3
- from otter_service_stdalone import fs_logging as log, upload_handle as uh
4
- import os
5
-
6
- log_debug = f'{os.environ.get("ENVIRONMENT")}-debug'
7
- log_count = f'{os.environ.get("ENVIRONMENT")}-count'
8
- log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
9
-
10
-
11
- class GradeNotebooks():
12
- """The class contains the async grade method for executing
13
- otter grader as well as a function for logging the number of
14
- notebooks to be graded
15
- """
16
-
17
- def count_ipynb_files(self, directory, extension):
18
- """this count the files for logging purposes"""
19
- count = 0
20
- for filename in os.listdir(directory):
21
- if filename.endswith(extension):
22
- count += 1
23
- return count
24
-
25
- async def grade(self, p, notebooks_path, results_id):
26
- """Calls otter grade asynchronously and writes the various log files
27
- and results of grading generating by otter-grader
28
-
29
- Args:
30
- p (str): the path to autograder.zip -- the solutions
31
- notebooks_path (str): the path to the folder of notebooks to be graded
32
- results_id (str): used for identifying logs
33
-
34
- Raises:
35
- Exception: Timeout Exception is raised if async takes longer than 20 min
36
-
37
- Returns:
38
- boolean: True is the process completes; otherwise an Exception is thrown
39
- """
40
- try:
41
- notebook_folder = uh.handle_upload(notebooks_path, results_id)
42
- notebook_count = self.count_ipynb_files(notebook_folder, ".ipynb")
43
- log.write_logs(results_id, f"{notebook_count}",
44
- "",
45
- "info",
46
- f'{os.environ.get("ENVIRONMENT")}-count')
47
- log.write_logs(results_id, "Step 5: Notebook Folder configured for grader",
48
- f"Notebook Folder: {notebook_folder}",
49
- "debug",
50
- log_debug)
51
- command = [
52
- 'otter', 'grade',
53
- '-n', 'grader',
54
- '-a', p,
55
- notebook_folder,
56
- "--ext", "ipynb",
57
- "--containers", "10",
58
- "--timeout", "15",
59
- "-o", notebook_folder,
60
- "-v"
61
- ]
62
- log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
63
- " ".join(command),
64
- "debug",
65
- log_debug)
66
- process = await asyncio.create_subprocess_exec(
67
- *command,
68
- stdin=asyncio.subprocess.PIPE,
69
- stdout=asyncio.subprocess.PIPE,
70
- stderr=asyncio.subprocess.PIPE
71
- )
72
-
73
- # this is waiting for communication back from the process
74
- # some images are quite big and take some time to build the first
75
- # time through - like 20 min for otter-grader
76
- async with async_timeout.timeout(2000):
77
- stdout, stderr = await process.communicate()
78
-
79
- with open(f"{notebook_folder}/grading-output.txt", "w") as f:
80
- for line in stdout.decode().splitlines():
81
- f.write(line + "\n")
82
- log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
83
- f"{notebook_folder}/grading-output.txt",
84
- "debug",
85
- log_debug)
86
- with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
87
- for line in stderr.decode().splitlines():
88
- f.write(line + "\n")
89
- log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
90
- f"{notebook_folder}/grading-logs.txt",
91
- "debug",
92
- log_debug)
93
- log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
94
- " ".join(command),
95
- "debug",
96
- log_debug)
97
- log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
98
- " ".join(command),
99
- "info",
100
- log_error)
101
- return True
102
- except asyncio.TimeoutError:
103
- raise Exception(f'Grading timed out for {notebook_folder}')
104
- except Exception as e:
105
- raise e