otter-service-stdalone 0.1.16__tar.gz → 0.1.18__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.
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/PKG-INFO +1 -1
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/setup.cfg +1 -1
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/__init__.py +1 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone/access_sops_keys.py +6 -6
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/app.py +235 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone/fs_logging.py +1 -1
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/grade_notebooks.py +103 -0
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.dev.yaml +17 -0
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.local.yaml +16 -0
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.prod.yaml +16 -0
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.staging.yaml +16 -0
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/static_templates/403.html +24 -0
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/static_templates/500.html +24 -0
- otter_service_stdalone-0.1.18/src/otter_service_stdalone/user_auth.py +120 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/PKG-INFO +1 -1
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/SOURCES.txt +8 -0
- otter_service_stdalone-0.1.16/src/otter_service_stdalone/__init__.py +0 -1
- otter_service_stdalone-0.1.16/src/otter_service_stdalone/app.py +0 -296
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/README.md +0 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/pyproject.toml +0 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone/index.html +0 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone/upload_handle.py +0 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/dependency_links.txt +0 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/entry_points.txt +0 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/top_level.txt +0 -0
- {otter_service_stdalone-0.1.16 → otter_service_stdalone-0.1.18}/tests/test_upload_handle.py +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.18"
|
@@ -14,10 +14,10 @@ def get(course_key, key_name, sops_path=None, secrets_file=None) -> str:
|
|
14
14
|
:return: the secret
|
15
15
|
"""
|
16
16
|
try:
|
17
|
-
|
18
|
-
if
|
19
|
-
|
20
|
-
return
|
17
|
+
sec = get_via_env(course_key, key_name)
|
18
|
+
if sec is None:
|
19
|
+
sec = get_via_sops(course_key, key_name, sops_path=sops_path, secrets_file=secrets_file)
|
20
|
+
return sec
|
21
21
|
except Exception as ex:
|
22
22
|
raise Exception(f"Key not decrypted: {key_name}; please configure: Error: {ex}") from ex
|
23
23
|
|
@@ -38,8 +38,8 @@ def get_via_sops(course_key, key, sops_path=None, secrets_file=None):
|
|
38
38
|
|
39
39
|
secrets_file = secrets_file
|
40
40
|
|
41
|
-
|
42
|
-
dct = yaml.safe_load(
|
41
|
+
sops_ot = subprocess.check_output([sops_path, "-d", secrets_file], stderr=subprocess.STDOUT)
|
42
|
+
dct = yaml.safe_load(sops_ot)
|
43
43
|
if course_key is None:
|
44
44
|
return dct[key]
|
45
45
|
return dct[course_key][key]
|
@@ -0,0 +1,235 @@
|
|
1
|
+
import tornado
|
2
|
+
import tornado.ioloop
|
3
|
+
import tornado.web
|
4
|
+
import tornado.auth
|
5
|
+
import os
|
6
|
+
import uuid
|
7
|
+
from otter_service_stdalone import fs_logging as log
|
8
|
+
from otter_service_stdalone import user_auth as u_auth
|
9
|
+
from otter_service_stdalone import grade_notebooks
|
10
|
+
from zipfile import ZipFile, ZIP_DEFLATED
|
11
|
+
|
12
|
+
|
13
|
+
__UPLOADS__ = "/tmp/uploads"
|
14
|
+
log_debug = f'{os.environ.get("ENVIRONMENT")}-debug'
|
15
|
+
log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
|
16
|
+
|
17
|
+
state = str(uuid.uuid4()) # used to protect against cross-site request forgery attacks.
|
18
|
+
|
19
|
+
|
20
|
+
class LoginHandler(tornado.web.RequestHandler):
|
21
|
+
"""Initiaties login auth by authorizing access to github auth api
|
22
|
+
|
23
|
+
Args:
|
24
|
+
tornado (tornado.web.RequestHandler): The request handler
|
25
|
+
"""
|
26
|
+
async def get(self):
|
27
|
+
await u_auth.handle_authorization(self, state)
|
28
|
+
|
29
|
+
|
30
|
+
class BaseHandler(tornado.web.RequestHandler):
|
31
|
+
"""This is the super class for the handlers. get_current_user is called by
|
32
|
+
any handler that decorated with @tornado.web.authenticated
|
33
|
+
|
34
|
+
Args:
|
35
|
+
tornado (tornado.web.RequestHandler): The request handler
|
36
|
+
"""
|
37
|
+
def get_current_user(self):
|
38
|
+
return self.get_secure_cookie("user")
|
39
|
+
|
40
|
+
def write_error(self, status_code, **kwargs):
|
41
|
+
log.write_logs("Http Error", f"{status_code} Error", "", "info", log_error)
|
42
|
+
if status_code == 403:
|
43
|
+
self.set_status(403)
|
44
|
+
self.render("static_templates/403.html")
|
45
|
+
else:
|
46
|
+
self.set_status(500)
|
47
|
+
self.render("static_templates/500.html")
|
48
|
+
|
49
|
+
|
50
|
+
class GitHubOAuthHandler(BaseHandler):
|
51
|
+
"""Handles GitHubOAuth
|
52
|
+
|
53
|
+
Args:
|
54
|
+
tornado (tornado.web.RequestHandler): The request handler
|
55
|
+
"""
|
56
|
+
async def get(self):
|
57
|
+
code = self.get_argument('code', False)
|
58
|
+
arg_state = self.get_argument('state', False)
|
59
|
+
access_token = await u_auth.get_acess_token(arg_state, state, code)
|
60
|
+
if access_token:
|
61
|
+
user = await u_auth.get_github_username(access_token)
|
62
|
+
is_org_member = await u_auth.handle_is_org_member(access_token, user)
|
63
|
+
if is_org_member:
|
64
|
+
self.set_secure_cookie("user", user, expires_days=7)
|
65
|
+
self.redirect("/")
|
66
|
+
else:
|
67
|
+
raise tornado.web.HTTPError(403)
|
68
|
+
else:
|
69
|
+
raise tornado.web.HTTPError(500)
|
70
|
+
|
71
|
+
|
72
|
+
class MainHandler(BaseHandler):
|
73
|
+
"""This is the initial landing page for application
|
74
|
+
|
75
|
+
Args:
|
76
|
+
BaseHandler (BaseHandler): super class
|
77
|
+
"""
|
78
|
+
@tornado.web.authenticated
|
79
|
+
async def get(self):
|
80
|
+
self.render("index.html", message=None)
|
81
|
+
|
82
|
+
|
83
|
+
class Download(BaseHandler):
|
84
|
+
"""The class handling a request to download results
|
85
|
+
|
86
|
+
Args:
|
87
|
+
tornado (tornado.web.RequestHandler): The download request handler
|
88
|
+
"""
|
89
|
+
@tornado.web.authenticated
|
90
|
+
def get(self):
|
91
|
+
# this just redirects to login and displays main page
|
92
|
+
self.render("index.html", message=None)
|
93
|
+
|
94
|
+
@tornado.web.authenticated
|
95
|
+
async def post(self):
|
96
|
+
"""the post method that accepts the code used to locate the results
|
97
|
+
the user wants to download
|
98
|
+
"""
|
99
|
+
download_code = self.get_argument('download')
|
100
|
+
directory = f"{__UPLOADS__}/{download_code}"
|
101
|
+
if download_code == "":
|
102
|
+
m = "Download: Code Not Given!"
|
103
|
+
log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
|
104
|
+
msg = "Please enter the download code to see your result."
|
105
|
+
self.render("index.html", download_message=msg)
|
106
|
+
elif not os.path.exists(f"{directory}"):
|
107
|
+
m = "Download: Directory for Code Not existing"
|
108
|
+
log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
|
109
|
+
msg = "The download code appears to not be correct or expired "
|
110
|
+
msg += f"- results are deleted regularly: {download_code}."
|
111
|
+
msg += "Please check the code or upload your notebooks "
|
112
|
+
msg += "and autograder.zip for grading again."
|
113
|
+
self.render("index.html", download_message=msg)
|
114
|
+
elif not os.path.exists(f"{directory}/grading-logs.txt"):
|
115
|
+
m = "Download: Results Not Ready"
|
116
|
+
log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
|
117
|
+
msg = "The results of your download are not ready yet. "
|
118
|
+
msg += "Please check back."
|
119
|
+
self.render("index.html", download_message=msg, dcode=download_code)
|
120
|
+
else:
|
121
|
+
if not os.path.isfile(f"{directory}/final_grades.csv"):
|
122
|
+
m = "Download: final_grades.csv does not exist"
|
123
|
+
t = "Problem grading notebooks see stack trace"
|
124
|
+
log.write_logs(download_code, m, t, "debug", log_debug)
|
125
|
+
with open(f"{directory}/final_grades.csv", "a") as f:
|
126
|
+
m = "There was a problem grading your notebooks. Please see grading-logs.txt"
|
127
|
+
f.write(m)
|
128
|
+
f.close()
|
129
|
+
m = "Download Success: Creating results.zip"
|
130
|
+
log.write_logs(download_code, m, "", "debug", log_debug)
|
131
|
+
with ZipFile(f"{directory}/results.zip", 'w') as zipF:
|
132
|
+
for file in ["final_grades.csv", "grading-logs.txt"]:
|
133
|
+
if os.path.isfile(f"{directory}/{file}"):
|
134
|
+
zipF.write(f"{directory}/{file}", file, compress_type=ZIP_DEFLATED)
|
135
|
+
|
136
|
+
self.set_header('Content-Type', 'application/octet-stream')
|
137
|
+
self.set_header("Content-Description", "File Transfer")
|
138
|
+
m = f"attachment; filename=results-{download_code}.zip"
|
139
|
+
self.set_header('Content-Disposition', m)
|
140
|
+
with open(f"{directory}/results.zip", 'rb') as f:
|
141
|
+
try:
|
142
|
+
while True:
|
143
|
+
data = f.read(4096)
|
144
|
+
if not data:
|
145
|
+
break
|
146
|
+
self.write(data)
|
147
|
+
self.finish()
|
148
|
+
except Exception as exc:
|
149
|
+
self.write(exc)
|
150
|
+
|
151
|
+
|
152
|
+
class Upload(BaseHandler):
|
153
|
+
"""This is the upload handler for users to upload autograder.zip and notebooks
|
154
|
+
|
155
|
+
Args:
|
156
|
+
tornado (tornado.web.RequestHandler): The upload request handler
|
157
|
+
"""
|
158
|
+
@tornado.web.authenticated
|
159
|
+
def get(self):
|
160
|
+
# this just redirects to login and displays main page
|
161
|
+
self.render("index.html", message=None)
|
162
|
+
|
163
|
+
@tornado.web.authenticated
|
164
|
+
async def post(self):
|
165
|
+
"""this handles the post request and asynchronously launches the grader
|
166
|
+
"""
|
167
|
+
g = grade_notebooks.GradeNotebooks()
|
168
|
+
files = self.request.files
|
169
|
+
results_path = str(uuid.uuid4())
|
170
|
+
autograder = self.request.files['autograder'][0] if "autograder" in files else None
|
171
|
+
notebooks = self.request.files['notebooks'][0] if "notebooks" in files else None
|
172
|
+
log.write_logs(results_path, "Step 1: Upload accepted", "", "debug", log_debug)
|
173
|
+
if autograder is not None and notebooks is not None:
|
174
|
+
notebooks_fname = notebooks['filename']
|
175
|
+
notebooks_extn = os.path.splitext(notebooks_fname)[1]
|
176
|
+
notebooks_name = results_path + notebooks_extn
|
177
|
+
autograder_fname = autograder['filename']
|
178
|
+
autograder_extn = os.path.splitext(autograder_fname)[1]
|
179
|
+
autograder_name = str(uuid.uuid4()) + autograder_extn
|
180
|
+
if not os.path.exists(__UPLOADS__):
|
181
|
+
os.mkdir(__UPLOADS__)
|
182
|
+
auto_p = f"{__UPLOADS__}/{autograder_name}"
|
183
|
+
notebooks_path = f"{__UPLOADS__}/{notebooks_name}"
|
184
|
+
m = "Step 2a: Uploaded File Names Determined"
|
185
|
+
log.write_logs(results_path, m, f"notebooks path: {notebooks_path}", "debug", log_debug)
|
186
|
+
fh = open(auto_p, 'wb')
|
187
|
+
fh.write(autograder['body'])
|
188
|
+
|
189
|
+
fh = open(notebooks_path, 'wb')
|
190
|
+
fh.write(notebooks['body'])
|
191
|
+
m = "Step 3: Uploaded Files Written to Disk"
|
192
|
+
log.write_logs(results_path, m, f"Results Code: {results_path}", "debug", log_debug)
|
193
|
+
m = "Please save this code. You can retrieve your files by submitting this code "
|
194
|
+
m += f"in the \"Results\" section to the right: {results_path}"
|
195
|
+
self.render("index.html", message=m)
|
196
|
+
try:
|
197
|
+
await g.grade(auto_p, notebooks_path, results_path)
|
198
|
+
except Exception as e:
|
199
|
+
log.write_logs(results_path, "Grading Problem", str(e), "error", log_error)
|
200
|
+
else:
|
201
|
+
m = "Step 2b: Uploaded Files not given"
|
202
|
+
log.write_logs(results_path, m, "", "debug", log_debug)
|
203
|
+
m = "It looks like you did not set the notebooks or autograder.zip or both!"
|
204
|
+
self.render("index.html", message=m)
|
205
|
+
|
206
|
+
|
207
|
+
settings = {
|
208
|
+
"cookie_secret": str(uuid.uuid4()),
|
209
|
+
"xsrf_cookies": True,
|
210
|
+
"login_url": "/login"
|
211
|
+
}
|
212
|
+
|
213
|
+
application = tornado.web.Application([
|
214
|
+
(r"/", MainHandler),
|
215
|
+
(r"/login", LoginHandler),
|
216
|
+
(r"/upload", Upload),
|
217
|
+
(r"/download", Download),
|
218
|
+
(r"/oauth_callback", GitHubOAuthHandler),
|
219
|
+
], **settings, debug=False)
|
220
|
+
|
221
|
+
|
222
|
+
def main():
|
223
|
+
"""the web servers entry point
|
224
|
+
"""
|
225
|
+
try:
|
226
|
+
application.listen(80)
|
227
|
+
log.write_logs("Server Start", "Starting Server", "", "info", log_debug)
|
228
|
+
tornado.ioloop.IOLoop.instance().start()
|
229
|
+
except Exception as e:
|
230
|
+
m = "Server Starting error"
|
231
|
+
log.write_logs("Server Start Error", m, str(e), "error", log_debug)
|
232
|
+
|
233
|
+
|
234
|
+
if __name__ == "__main__":
|
235
|
+
main()
|
@@ -50,7 +50,7 @@ def write_logs(id, msg, trace, type, collection):
|
|
50
50
|
try:
|
51
51
|
db = firestore.client()
|
52
52
|
# this redirects FireStore to local emulator when local testing!
|
53
|
-
if os.getenv("ENVIRONMENT")
|
53
|
+
if "local" in os.getenv("ENVIRONMENT"):
|
54
54
|
channel = grpc.insecure_channel("host.docker.internal:8080")
|
55
55
|
transport = firestore_grpc_transport.FirestoreGrpcTransport(channel=channel)
|
56
56
|
db._firestore_api_internal = firestore_client.FirestoreClient(transport=transport)
|
@@ -0,0 +1,103 @@
|
|
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
|
+
'-a', p,
|
54
|
+
'-p', notebook_folder,
|
55
|
+
"--ext", "ipynb",
|
56
|
+
"--containers", "10",
|
57
|
+
"-o", notebook_folder,
|
58
|
+
"-v"
|
59
|
+
]
|
60
|
+
log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
|
61
|
+
" ".join(command),
|
62
|
+
"debug",
|
63
|
+
log_debug)
|
64
|
+
process = await asyncio.create_subprocess_exec(
|
65
|
+
*command,
|
66
|
+
stdin=asyncio.subprocess.PIPE,
|
67
|
+
stdout=asyncio.subprocess.PIPE,
|
68
|
+
stderr=asyncio.subprocess.PIPE
|
69
|
+
)
|
70
|
+
|
71
|
+
# this is waiting for communication back from the process
|
72
|
+
# some images are quite big and take some time to build the first
|
73
|
+
# time through - like 20 min for otter-grader
|
74
|
+
async with async_timeout.timeout(2000):
|
75
|
+
stdout, stderr = await process.communicate()
|
76
|
+
|
77
|
+
with open(f"{notebook_folder}/grading-output.txt", "w") as f:
|
78
|
+
for line in stdout.decode().splitlines():
|
79
|
+
f.write(line + "\n")
|
80
|
+
log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
|
81
|
+
f"{notebook_folder}/grading-output.txt",
|
82
|
+
"debug",
|
83
|
+
log_debug)
|
84
|
+
with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
|
85
|
+
for line in stderr.decode().splitlines():
|
86
|
+
f.write(line + "\n")
|
87
|
+
log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
|
88
|
+
f"{notebook_folder}/grading-logs.txt",
|
89
|
+
"debug",
|
90
|
+
log_debug)
|
91
|
+
log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
|
92
|
+
" ".join(command),
|
93
|
+
"debug",
|
94
|
+
log_debug)
|
95
|
+
log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
|
96
|
+
" ".join(command),
|
97
|
+
"info",
|
98
|
+
log_error)
|
99
|
+
return True
|
100
|
+
except asyncio.TimeoutError:
|
101
|
+
raise Exception(f'Grading timed out for {notebook_folder}')
|
102
|
+
except Exception as e:
|
103
|
+
raise e
|
@@ -0,0 +1,17 @@
|
|
1
|
+
github_access_id: ENC[AES256_GCM,data:L97KjBroMsmVUYBOwIhGirB2nns=,iv:ZcBj74anvs43HziVBeQQRwl7hoEtX36+2wEQbFPNS0c=,tag:ptO+Tsx37xk1pWAGjctuhQ==,type:str]
|
2
|
+
github_access_secret: ENC[AES256_GCM,data:5DuzOLRI8HwW5KhskZQW+HzwPiIHBlYcBCMpfAt34dZg9lOB+ivrcw==,iv:Y4H6MrJT1dnO1HsOy6XYDQfUERXCssu+7/FP5SEEnCw=,tag:UYp6KrHWG4XrxY8LroSO6Q==,type:str]
|
3
|
+
sops:
|
4
|
+
kms: []
|
5
|
+
gcp_kms:
|
6
|
+
- resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
|
7
|
+
created_at: "2024-04-03T18:29:45Z"
|
8
|
+
enc: CiUA67O9AArlsf5j4XCvPmf4qs3txe7+SxbVuJggNgW3GLh2DdACEkkAYHATHigxI73bJa6VfxZDxj5KLXsnz0ql72q7r7dzgSCwwjtFRrqizyloq2JvIoJskpjBcZBIe4VVVXHXsg8TUX/iMEB2G0Wm
|
9
|
+
azure_kv: []
|
10
|
+
hc_vault: []
|
11
|
+
age: []
|
12
|
+
lastmodified: "2024-04-03T18:29:46Z"
|
13
|
+
mac: ENC[AES256_GCM,data:QWZetePfWsqQQtCKc6O++f1falvJujzYMhxqRQFPmoUmQrFHBZ06v4XPPUciZGcrSgkhuyQwgHYXXSap1rDba2/D2i8jy8n8jC+hop60HLsveY53+ahYu3Fs91iQh3UdYg5ecTE0I/6prCdqVNL8nshYSBILYVNVG4KJAI7+G4s=,iv:4Beqcu1nYExqiv58Cs/YlCFGS1UV/MkAC5SWdmHr9Jw=,tag:h5PXnYsDtTFBFqkfGpPaJw==,type:str]
|
14
|
+
pgp: []
|
15
|
+
unencrypted_suffix: _unencrypted
|
16
|
+
version: 3.7.1
|
17
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
github_access_id: ENC[AES256_GCM,data:MULsaNCCrl+bF/HvLO00HKfCw54=,iv:Wi4txCUYoM+Zi3CHTSjqSuGAwUToZdggDOVuaZndHLA=,tag:CtKnanwlrdXLnPs74ZsqOQ==,type:str]
|
2
|
+
github_access_secret: ENC[AES256_GCM,data:h/cKH4nRjylg8ELxXsTvRuEuuB0P87R6SLqrbEBkQrjjgT+Bv/SVhQ==,iv:WmFvG6nijf9u0NE3ykJDaVxLdhbnDeiXbnexlYJDClE=,tag:HSO2CzLpCiqJJ9UdLn3+aw==,type:str]
|
3
|
+
sops:
|
4
|
+
kms: []
|
5
|
+
gcp_kms:
|
6
|
+
- resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
|
7
|
+
created_at: "2024-03-26T22:42:25Z"
|
8
|
+
enc: CiUA67O9AORb6b4pqLYE7IAfXT1a6ZHA9TycUT/TmUecInMxiq8aEkkAYHATHnnEuC/W1e4eckSfEz+yQe5Eu7CrX7X8f8DTBIFLLyu6tFgSKjr3VxitXNmBhOY5rQ53e+M/QbgjeJ2LexKjuWCFck+A
|
9
|
+
azure_kv: []
|
10
|
+
hc_vault: []
|
11
|
+
age: []
|
12
|
+
lastmodified: "2024-03-26T22:42:26Z"
|
13
|
+
mac: ENC[AES256_GCM,data:n0QV+da9B/zogfbFCjY94Sr+uaTZBUjyjIvqOh7nIf/OCTjLwN6dsJlF6QCLlWIChTS+hfPn+8gEmJpn7gTrJIYfzO77ByP2hIxu+CAjLGHJ4V3/QrY3odPPzbiRv6KXpVpCt/JWXGcoimKMeFStwtaUDDWqh/xDbb7UwciXFxE=,iv:GAML0oOeBpI2SUW3NkNNJbWlhGBP4AyjUlZVG+qnds0=,tag:+1xj3mMmL33pBSWT6NyAjw==,type:str]
|
14
|
+
pgp: []
|
15
|
+
unencrypted_suffix: _unencrypted
|
16
|
+
version: 3.7.1
|
@@ -0,0 +1,16 @@
|
|
1
|
+
github_access_id: ENC[AES256_GCM,data:c8f28uHdSyPTkYqGeYQLTVilBfM=,iv:xF06JH1aACwzifY6dLpGyNC/Gp4UfUrRHhHEKRq86RQ=,tag:crN2FEg5rcam9XlWO26Bag==,type:str]
|
2
|
+
github_access_secret: ENC[AES256_GCM,data:paO5S9N9cn4/fiyG6P16550hL/r7dz8EHRMJLTaPdZgBtrHnaMq6HQ==,iv:qPAEDBcRJYyq0tPj7vPDPd+x1zhEhaTZm7TTOW3JNyU=,tag:9lKfrBt1Fnaq9T5J8jy4Hw==,type:str]
|
3
|
+
sops:
|
4
|
+
kms: []
|
5
|
+
gcp_kms:
|
6
|
+
- resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
|
7
|
+
created_at: "2024-04-03T18:24:30Z"
|
8
|
+
enc: CiUA67O9AKNwFJ3SWLlDu7J9GHFv6BLZeu15atOKMwR5Z1pXtaPEEkkAYHATHmUfzxyp7nC6ly2ICTtW6rd7b9qG4w2N2n4rlC+0/eT0MjTQIDB4WBa65YGfp+8dYQXS6S6sBTK+ozbX6nXtcinddCcr
|
9
|
+
azure_kv: []
|
10
|
+
hc_vault: []
|
11
|
+
age: []
|
12
|
+
lastmodified: "2024-04-03T18:24:31Z"
|
13
|
+
mac: ENC[AES256_GCM,data:kCdn15/6xjxE3YYJMNJJaLmP7gswC2gEPop+ptkyq+TPu1ufG6kGaCaDbXScr6w7Qe/RXnzdeOdTp/nF7nSRL9YRH6acR2RXcWuV6ngWFUM+YJXgiM6uDB1Ps+yn2OdzEa1v8LZo0gGRxraHdScqTxJ0bL7koFQBKiJf7CWdhD8=,iv:VoFzGQLkufxBFZVtGMxK9x2jF2jIpVPvjdWB8Z3Edo4=,tag:KCix8V1a7V9vCbt4A7kxtA==,type:str]
|
14
|
+
pgp: []
|
15
|
+
unencrypted_suffix: _unencrypted
|
16
|
+
version: 3.7.1
|
@@ -0,0 +1,16 @@
|
|
1
|
+
github_access_id: ENC[AES256_GCM,data:3R9AMcMccaaF+1z3zuu2aehmJHI=,iv:mnhhWTTXsbAJN0FdoFzpc8WrImYLSM1chk3lqwxFSkw=,tag:RSBoplL8Ve7MjTW6Zz7WMQ==,type:str]
|
2
|
+
github_access_secret: ENC[AES256_GCM,data:+ebdQ0Jxy/bRe5plcuZZIrDoULu02jE+vj2HxoERYDG462YRONtiww==,iv:fHLqi4D1ixCdoOK5NASkkVLdX7kExnh8nOIOPaksnN0=,tag:9Jl2lJajSzO4UzJ/T+A8sw==,type:str]
|
3
|
+
sops:
|
4
|
+
kms: []
|
5
|
+
gcp_kms:
|
6
|
+
- resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
|
7
|
+
created_at: "2024-04-03T18:27:16Z"
|
8
|
+
enc: CiUA67O9AKF7k4zHdbhRYNzT1cKTzkS1by8G/1516HbJQBlOsnf2EkkAYHATHos+I2G4tn7GIRJhjrsXLeN++ZUUyICCn3VB1Wzcm2udHOfce4GGGc0XcQc18Nvn1Zr/ToroSv9qI8DarocbEHZX+H/l
|
9
|
+
azure_kv: []
|
10
|
+
hc_vault: []
|
11
|
+
age: []
|
12
|
+
lastmodified: "2024-04-03T18:27:17Z"
|
13
|
+
mac: ENC[AES256_GCM,data:Q/A3QkCxo1GdcSB/gtAyB3DFaXKrcAtpzXpe6GJYD8jD1v5N7rAW/DFo459r5jfAM+9W4Ks3cwTheBJgJnqxfHRuWp6FcP6GefNuScGsiRequp/FQwJcTMnAgLOGCsOGbSfzzsNVEm7j0VpkTkgavr/rFrqAlVC1MFT++bJsXTw=,iv:P86phogwEJL0ZMkBWH+qJWS8MyHbI1/0iM399cyuLxQ=,tag:MdO0PHXoe893Mk0GTfms+g==,type:str]
|
14
|
+
pgp: []
|
15
|
+
unencrypted_suffix: _unencrypted
|
16
|
+
version: 3.7.1
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
4
|
+
<title>403 Forbidden</title>
|
5
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
6
|
+
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<section class="container">
|
10
|
+
<div class="hero is-info welcome is-small">
|
11
|
+
<div class="hero-body">
|
12
|
+
<div class="container">
|
13
|
+
<h1 class="title">403 Forbidden</h1>
|
14
|
+
<p class="subtitle colored is-3">
|
15
|
+
You do not have permission to access this resource or your authentication needs to be renewed.
|
16
|
+
Please <a style='color:#005b96' href="/login">login</a> to try again or
|
17
|
+
email sean.smorris@berkeley.edu for help.
|
18
|
+
</p>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</section>
|
23
|
+
</body>
|
24
|
+
</html>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
4
|
+
<title>Access Problem</title>
|
5
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
6
|
+
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<section class="container">
|
10
|
+
<div class="hero is-info welcome is-small">
|
11
|
+
<div class="hero-body">
|
12
|
+
<div class="container">
|
13
|
+
<h1 class="title">500 Forbidden - Access Problem</h1>
|
14
|
+
<p class="subtitle colored is-3">
|
15
|
+
We are unable to establish access for you.
|
16
|
+
Please email: sean.smorris@berkeley.edu or try
|
17
|
+
<a style='color:#005b96' href="/login">logging</a> in again.
|
18
|
+
</p>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</section>
|
23
|
+
</body>
|
24
|
+
</html>
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from otter_service_stdalone import fs_logging as log, access_sops_keys
|
4
|
+
import requests
|
5
|
+
import tornado
|
6
|
+
import urllib.parse
|
7
|
+
|
8
|
+
|
9
|
+
log_coll = f'{os.environ.get("ENVIRONMENT")}-debug'
|
10
|
+
environment_name = os.environ.get("ENVIRONMENT").split("-")[-1]
|
11
|
+
gh_key_path = os.path.join(os.path.dirname(__file__), f"secrets/gh_key.{environment_name}.yaml")
|
12
|
+
github_id = access_sops_keys.get(None, "github_access_id", secrets_file=gh_key_path)
|
13
|
+
github_secret = access_sops_keys.get(None, "github_access_secret", secrets_file=gh_key_path)
|
14
|
+
|
15
|
+
|
16
|
+
async def handle_authorization(form, state):
|
17
|
+
"""authorizes via oauth app token access github auth api
|
18
|
+
|
19
|
+
Parameters:
|
20
|
+
- form (tornado.web.RequestHandler): The request handler used to re-direct for
|
21
|
+
authorization. The redirect url is configured at the github endpoint for the app
|
22
|
+
- state (int): random uuid4 number generated to ensure communication between endpoints is
|
23
|
+
not compromised
|
24
|
+
"""
|
25
|
+
log.write_logs("Auth Workflow", "UserAuth: Get: Authorizing", "", "info", log_coll)
|
26
|
+
q_params = f"client_id={github_id}&state={state}&scope=read:org"
|
27
|
+
form.redirect(f'https://github.com/login/oauth/authorize?{q_params}')
|
28
|
+
|
29
|
+
|
30
|
+
async def get_acess_token(arg_state, state, code):
|
31
|
+
"""requests and returns the access token or None
|
32
|
+
|
33
|
+
Parameters:
|
34
|
+
- arg_state (int): the state argument being passed on the url string; this will be compared
|
35
|
+
to the state value generated in this file to ensure they are the same!
|
36
|
+
- state (int): random uuid4 number generated to ensure communication between endpoints is
|
37
|
+
not compromised
|
38
|
+
- code (str): the code that is returned from the authorization request
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
- str: the access token used for subsequent calls to the api; or None
|
42
|
+
"""
|
43
|
+
http_client = tornado.httpclient.AsyncHTTPClient()
|
44
|
+
params = {
|
45
|
+
'client_id': github_id,
|
46
|
+
'client_secret': github_secret,
|
47
|
+
'code': code,
|
48
|
+
'redirect_uri': f"{os.environ.get('GRADER_DNS')}/oauth_callback"
|
49
|
+
}
|
50
|
+
m = "UserAuth: GitHubOAuthHandler: Getting Access Token"
|
51
|
+
log.write_logs("Auth Workflow", m, "", "info", log_coll)
|
52
|
+
response = await http_client.fetch(
|
53
|
+
'https://github.com/login/oauth/access_token',
|
54
|
+
method='POST',
|
55
|
+
headers={'Accept': 'application/json'},
|
56
|
+
body=urllib.parse.urlencode(params)
|
57
|
+
)
|
58
|
+
resp = json.loads(response.body.decode())
|
59
|
+
access_token = resp['access_token']
|
60
|
+
if arg_state != state:
|
61
|
+
access_token = None
|
62
|
+
m = "UserAuth: GitHubOAuthHandler: Cross-Site Forgery possible - aborting"
|
63
|
+
log.write_logs("Auth Workflow", m, "", "info", log_coll)
|
64
|
+
else:
|
65
|
+
m = "UserAuth: GitHubOAuthHandler: Access Token Granted"
|
66
|
+
log.write_logs("Auth Workflow", m, "", "info", log_coll)
|
67
|
+
return access_token
|
68
|
+
|
69
|
+
|
70
|
+
async def handle_is_org_member(access_token, user):
|
71
|
+
"""the final authorization is to make sure the member has access to the application by being
|
72
|
+
a part of the correct organizaton
|
73
|
+
|
74
|
+
Parameters:
|
75
|
+
- access_token (str): The OAuth access token.
|
76
|
+
- user (str): the authenticated GitHub user name
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
- boolean: True user is in the GH org, False otherwise
|
80
|
+
"""
|
81
|
+
log.write_logs("Auth Workflow", "UserAuth: Get: Check Membership", "", "info", log_coll)
|
82
|
+
if user:
|
83
|
+
org_name = os.environ.get("AUTH_ORG")
|
84
|
+
url = f'https://api.github.com/orgs/{org_name}/members/{user}'
|
85
|
+
headers = {
|
86
|
+
'Authorization': f'token {access_token}',
|
87
|
+
'Accept': 'application/vnd.github.v3+json',
|
88
|
+
}
|
89
|
+
response = requests.get(url, headers=headers)
|
90
|
+
return response.status_code == 204
|
91
|
+
else:
|
92
|
+
return False
|
93
|
+
|
94
|
+
|
95
|
+
async def get_github_username(access_token):
|
96
|
+
"""
|
97
|
+
Retrieve the GitHub username of the authenticated user.
|
98
|
+
|
99
|
+
Parameters:
|
100
|
+
- access_token (str): The OAuth access token.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
- str: The username of the authenticated user, or None if not found.
|
104
|
+
"""
|
105
|
+
url = 'https://api.github.com/user'
|
106
|
+
headers = {
|
107
|
+
'Authorization': f'token {access_token}',
|
108
|
+
'Accept': 'application/vnd.github.v3+json',
|
109
|
+
}
|
110
|
+
|
111
|
+
response = requests.get(url, headers=headers)
|
112
|
+
if response.status_code == 200:
|
113
|
+
m = "UserAuth: Get: UserName - Success"
|
114
|
+
log.write_logs("Auth Workflow", m, "", "info", log_coll)
|
115
|
+
user_info = response.json()
|
116
|
+
return user_info.get('login')
|
117
|
+
else:
|
118
|
+
m = f"UserAuth: Get: UserName - Fail:{response.status_code}"
|
119
|
+
log.write_logs("Auth Workflow", m, "", "info", log_coll)
|
120
|
+
return None
|
@@ -5,11 +5,19 @@ src/otter_service_stdalone/__init__.py
|
|
5
5
|
src/otter_service_stdalone/access_sops_keys.py
|
6
6
|
src/otter_service_stdalone/app.py
|
7
7
|
src/otter_service_stdalone/fs_logging.py
|
8
|
+
src/otter_service_stdalone/grade_notebooks.py
|
8
9
|
src/otter_service_stdalone/index.html
|
9
10
|
src/otter_service_stdalone/upload_handle.py
|
11
|
+
src/otter_service_stdalone/user_auth.py
|
10
12
|
src/otter_service_stdalone.egg-info/PKG-INFO
|
11
13
|
src/otter_service_stdalone.egg-info/SOURCES.txt
|
12
14
|
src/otter_service_stdalone.egg-info/dependency_links.txt
|
13
15
|
src/otter_service_stdalone.egg-info/entry_points.txt
|
14
16
|
src/otter_service_stdalone.egg-info/top_level.txt
|
17
|
+
src/otter_service_stdalone/secrets/gh_key.dev.yaml
|
18
|
+
src/otter_service_stdalone/secrets/gh_key.local.yaml
|
19
|
+
src/otter_service_stdalone/secrets/gh_key.prod.yaml
|
20
|
+
src/otter_service_stdalone/secrets/gh_key.staging.yaml
|
21
|
+
src/otter_service_stdalone/static_templates/403.html
|
22
|
+
src/otter_service_stdalone/static_templates/500.html
|
15
23
|
tests/test_upload_handle.py
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.1.16"
|
@@ -1,296 +0,0 @@
|
|
1
|
-
import tornado
|
2
|
-
import tornado.ioloop
|
3
|
-
import tornado.web
|
4
|
-
import tornado.auth
|
5
|
-
import os
|
6
|
-
import uuid
|
7
|
-
from otter_service_stdalone import fs_logging as log, upload_handle as uh
|
8
|
-
from zipfile import ZipFile, ZIP_DEFLATED
|
9
|
-
import asyncio
|
10
|
-
import async_timeout
|
11
|
-
import urllib.parse
|
12
|
-
from otter_service_stdalone import access_sops_keys
|
13
|
-
import json
|
14
|
-
|
15
|
-
__UPLOADS__ = "/tmp/uploads"
|
16
|
-
|
17
|
-
|
18
|
-
class GradeNotebooks():
|
19
|
-
"""The class contains the async grade method for executing
|
20
|
-
otter grader
|
21
|
-
"""
|
22
|
-
async def grade(self, p, notebooks_path, results_id):
|
23
|
-
"""Calls otter grade asynchronously and writes the various log files
|
24
|
-
and results of grading generating by otter-grader
|
25
|
-
|
26
|
-
Args:
|
27
|
-
p (str): the path to autograder.zip -- the solutions
|
28
|
-
notebooks_path (str): the path to the folder of notebooks to be graded
|
29
|
-
results_id (str): used for identifying logs
|
30
|
-
|
31
|
-
Raises:
|
32
|
-
Exception: Timeout Exception is raised if async takes longer than 20 min
|
33
|
-
|
34
|
-
Returns:
|
35
|
-
boolean: True is the process completes; otherwise an Exception is thrown
|
36
|
-
"""
|
37
|
-
try:
|
38
|
-
notebook_folder = uh.handle_upload(notebooks_path, results_id)
|
39
|
-
log.write_logs(results_id, "Step 5: Notebook Folder configured for grader",
|
40
|
-
f"Notebook Folder: {notebook_folder}",
|
41
|
-
"debug",
|
42
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
43
|
-
command = [
|
44
|
-
'otter', 'grade',
|
45
|
-
'-a', p,
|
46
|
-
'-p', notebook_folder,
|
47
|
-
"--ext", "ipynb",
|
48
|
-
"--containers", "10",
|
49
|
-
"-o", notebook_folder,
|
50
|
-
"-v"
|
51
|
-
]
|
52
|
-
log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
|
53
|
-
" ".join(command),
|
54
|
-
"debug",
|
55
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
56
|
-
process = await asyncio.create_subprocess_exec(
|
57
|
-
*command,
|
58
|
-
stdin=asyncio.subprocess.PIPE,
|
59
|
-
stdout=asyncio.subprocess.PIPE,
|
60
|
-
stderr=asyncio.subprocess.PIPE
|
61
|
-
)
|
62
|
-
|
63
|
-
# this is waiting for communication back from the process
|
64
|
-
# some images are quite big and take some time to build the first
|
65
|
-
# time through - like 20 min for otter-grader
|
66
|
-
async with async_timeout.timeout(2000):
|
67
|
-
stdout, stderr = await process.communicate()
|
68
|
-
|
69
|
-
with open(f"{notebook_folder}/grading-output.txt", "w") as f:
|
70
|
-
for line in stdout.decode().splitlines():
|
71
|
-
f.write(line + "\n")
|
72
|
-
log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
|
73
|
-
f"{notebook_folder}/grading-output.txt",
|
74
|
-
"debug",
|
75
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
76
|
-
with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
|
77
|
-
for line in stderr.decode().splitlines():
|
78
|
-
f.write(line + "\n")
|
79
|
-
log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
|
80
|
-
f"{notebook_folder}/grading-logs.txt",
|
81
|
-
"debug",
|
82
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
83
|
-
log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
|
84
|
-
" ".join(command),
|
85
|
-
"debug",
|
86
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
87
|
-
log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
|
88
|
-
" ".join(command),
|
89
|
-
"info",
|
90
|
-
f'{os.environ.get("ENVIRONMENT")}-logs')
|
91
|
-
return True
|
92
|
-
except asyncio.TimeoutError:
|
93
|
-
raise Exception(f'Grading timed out for {notebook_folder}')
|
94
|
-
except Exception as e:
|
95
|
-
raise e
|
96
|
-
|
97
|
-
|
98
|
-
class Userform(tornado.web.RequestHandler):
|
99
|
-
"""This is the initial landing page for application
|
100
|
-
|
101
|
-
Args:
|
102
|
-
tornado (tornado.web.RequestHandler): The request handler
|
103
|
-
"""
|
104
|
-
async def get(self):
|
105
|
-
# User will be redirected here by GitHub after authorization
|
106
|
-
code = self.get_argument('code', False)
|
107
|
-
if code:
|
108
|
-
access_token = await self.get_authenticated_user(code)
|
109
|
-
self.write(f'Your GitHub access token is: {access_token}')
|
110
|
-
else:
|
111
|
-
self.render("index.html", message=None)
|
112
|
-
|
113
|
-
async def get_authenticated_user(self, code):
|
114
|
-
secrets_file = os.path.join(os.path.dirname(__file__), "secrets/gh_key.yaml")
|
115
|
-
github_id = access_sops_keys.get(None, "github_access_id", secrets_file=secrets_file)
|
116
|
-
github_secret = access_sops_keys.get(None, "github_access_secret", secrets_file=secrets_file)
|
117
|
-
|
118
|
-
http_client = tornado.httpclient.AsyncHTTPClient()
|
119
|
-
params = {
|
120
|
-
'client_id': github_id,
|
121
|
-
'client_secret': github_secret,
|
122
|
-
'code': code,
|
123
|
-
'redirect_uri': "https://grader.datahub.berkeley.edu/oauth_callback"
|
124
|
-
}
|
125
|
-
response = await http_client.fetch(
|
126
|
-
'https://github.com/login/oauth/access_token',
|
127
|
-
method='POST',
|
128
|
-
headers={'Accept': 'application/json'},
|
129
|
-
body=urllib.parse.urlencode(params)
|
130
|
-
)
|
131
|
-
return json.loads(response.body.decode())['access_token']
|
132
|
-
|
133
|
-
|
134
|
-
class Download(tornado.web.RequestHandler):
|
135
|
-
"""The class handling a request to download results
|
136
|
-
|
137
|
-
Args:
|
138
|
-
tornado (tornado.web.RequestHandler): The download request handler
|
139
|
-
"""
|
140
|
-
async def post(self):
|
141
|
-
"""the post method that accepts the code used to locate the results
|
142
|
-
the user wants to download
|
143
|
-
"""
|
144
|
-
code = self.get_argument('download')
|
145
|
-
directory = f"{__UPLOADS__}/{code}"
|
146
|
-
if code == "":
|
147
|
-
log.write_logs(code, "Download: Code Not Given!",
|
148
|
-
f"{code}",
|
149
|
-
"debug",
|
150
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
151
|
-
msg = "Please enter the download code to see your result."
|
152
|
-
self.render("index.html", download_message=msg)
|
153
|
-
elif not os.path.exists(f"{directory}"):
|
154
|
-
log.write_logs(code, "Download: Directory for Code Not existing",
|
155
|
-
f"{code}",
|
156
|
-
"debug",
|
157
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
158
|
-
msg = "The download code appears to not be correct or expired "
|
159
|
-
msg += f"- results are deleted regularly: {code}."
|
160
|
-
msg += "Please check the code or upload your notebooks "
|
161
|
-
msg += "and autograder.zip for grading again."
|
162
|
-
self.render("index.html", download_message=msg)
|
163
|
-
elif not os.path.exists(f"{directory}/grading-logs.txt"):
|
164
|
-
log.write_logs(code, "Download: Results Not Ready",
|
165
|
-
f"{code}",
|
166
|
-
"debug",
|
167
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
168
|
-
msg = "The results of your download are not ready yet. "
|
169
|
-
msg += "Please check back."
|
170
|
-
self.render("index.html", download_message=msg, dcode=code)
|
171
|
-
else:
|
172
|
-
if not os.path.isfile(f"{directory}/final_grades.csv"):
|
173
|
-
log.write_logs(code, "Download: final_grades.csv does not exist",
|
174
|
-
"Problem grading notebooks see stack trace",
|
175
|
-
"debug",
|
176
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
177
|
-
with open(f"{directory}/final_grades.csv", "a") as f:
|
178
|
-
m = "There was a problem grading your notebooks. Please see grading-logs.txt"
|
179
|
-
f.write(m)
|
180
|
-
f.close()
|
181
|
-
|
182
|
-
log.write_logs(code, "Download Success: Creating results.zip",
|
183
|
-
"",
|
184
|
-
"debug",
|
185
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
186
|
-
with ZipFile(f"{directory}/results.zip", 'w') as zipF:
|
187
|
-
for file in ["final_grades.csv", "grading-logs.txt"]:
|
188
|
-
if os.path.isfile(f"{directory}/{file}"):
|
189
|
-
zipF.write(f"{directory}/{file}", file, compress_type=ZIP_DEFLATED)
|
190
|
-
|
191
|
-
self.set_header('Content-Type', 'application/octet-stream')
|
192
|
-
self.set_header("Content-Description", "File Transfer")
|
193
|
-
self.set_header('Content-Disposition', f"attachment; filename=results-{code}.zip")
|
194
|
-
with open(f"{directory}/results.zip", 'rb') as f:
|
195
|
-
try:
|
196
|
-
while True:
|
197
|
-
data = f.read(4096)
|
198
|
-
if not data:
|
199
|
-
break
|
200
|
-
self.write(data)
|
201
|
-
self.finish()
|
202
|
-
except Exception as exc:
|
203
|
-
self.write(exc)
|
204
|
-
|
205
|
-
|
206
|
-
class Upload(tornado.web.RequestHandler):
|
207
|
-
"""This is the upload handler for users to upload autograder.zip and notebooks
|
208
|
-
|
209
|
-
Args:
|
210
|
-
tornado (tornado.web.RequestHandler): The upload request handler
|
211
|
-
"""
|
212
|
-
async def post(self):
|
213
|
-
"""this handles the post request and asynchronously launches the grader
|
214
|
-
"""
|
215
|
-
g = GradeNotebooks()
|
216
|
-
files = self.request.files
|
217
|
-
results_path = str(uuid.uuid4())
|
218
|
-
autograder = self.request.files['autograder'][0] if "autograder" in files else None
|
219
|
-
notebooks = self.request.files['notebooks'][0] if "notebooks" in files else None
|
220
|
-
log.write_logs(results_path, "Step 1: Upload accepted",
|
221
|
-
"",
|
222
|
-
"debug",
|
223
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
224
|
-
if autograder is not None and notebooks is not None:
|
225
|
-
notebooks_fname = notebooks['filename']
|
226
|
-
notebooks_extn = os.path.splitext(notebooks_fname)[1]
|
227
|
-
notebooks_name = results_path + notebooks_extn
|
228
|
-
autograder_fname = autograder['filename']
|
229
|
-
autograder_extn = os.path.splitext(autograder_fname)[1]
|
230
|
-
autograder_name = str(uuid.uuid4()) + autograder_extn
|
231
|
-
if not os.path.exists(__UPLOADS__):
|
232
|
-
os.mkdir(__UPLOADS__)
|
233
|
-
auto_p = f"{__UPLOADS__}/{autograder_name}"
|
234
|
-
notebooks_path = f"{__UPLOADS__}/{notebooks_name}"
|
235
|
-
log.write_logs(results_path, "Step 2a: Uploaded File Names Determined",
|
236
|
-
f"notebooks path: {notebooks_path}",
|
237
|
-
"debug",
|
238
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
239
|
-
fh = open(auto_p, 'wb')
|
240
|
-
fh.write(autograder['body'])
|
241
|
-
|
242
|
-
fh = open(notebooks_path, 'wb')
|
243
|
-
fh.write(notebooks['body'])
|
244
|
-
log.write_logs(results_path, "Step 3: Uploaded Files Written to Disk",
|
245
|
-
f"Results Code: {results_path}",
|
246
|
-
"debug",
|
247
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
248
|
-
m = "Please save this code. You can retrieve your files by submitting this code "
|
249
|
-
m += f"in the \"Results\" section to the right: {results_path}"
|
250
|
-
self.render("index.html", message=m)
|
251
|
-
try:
|
252
|
-
await g.grade(auto_p, notebooks_path, results_path)
|
253
|
-
except Exception as e:
|
254
|
-
log.write_logs(results_path, "Grading Problem",
|
255
|
-
str(e),
|
256
|
-
"error",
|
257
|
-
f'{os.environ.get("ENVIRONMENT")}-logs')
|
258
|
-
else:
|
259
|
-
log.write_logs(results_path, "Step 2b: Uploaded Files not given",
|
260
|
-
"",
|
261
|
-
"debug",
|
262
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
263
|
-
m = "It looks like you did not set the notebooks or autograder.zip or both!"
|
264
|
-
self.render("index.html", message=m)
|
265
|
-
|
266
|
-
|
267
|
-
settings = {
|
268
|
-
"cookie_secret": str(uuid.uuid4()),
|
269
|
-
"xsrf_cookies": True
|
270
|
-
}
|
271
|
-
|
272
|
-
application = tornado.web.Application([
|
273
|
-
(r"/", Userform),
|
274
|
-
(r"/upload", Upload),
|
275
|
-
(r"/download", Download),
|
276
|
-
(r"/auth/github", Userform),
|
277
|
-
], **settings, debug=True)
|
278
|
-
|
279
|
-
|
280
|
-
def main():
|
281
|
-
"""the web servers entry point
|
282
|
-
"""
|
283
|
-
try:
|
284
|
-
application.listen(80)
|
285
|
-
msg = f'{os.environ.get("ENVIRONMENT")}-debug'
|
286
|
-
log.write_logs("Server Start", "Starting Server", "", "info", msg)
|
287
|
-
tornado.ioloop.IOLoop.instance().start()
|
288
|
-
except Exception as e:
|
289
|
-
log.write_logs("Server Start Error", "Server Starting error",
|
290
|
-
str(e),
|
291
|
-
"error",
|
292
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
293
|
-
|
294
|
-
|
295
|
-
if __name__ == "__main__":
|
296
|
-
main()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|