otter-service-stdalone 0.1.16__py3-none-any.whl → 0.1.18__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.
- otter_service_stdalone/__init__.py +1 -1
- otter_service_stdalone/access_sops_keys.py +6 -6
- otter_service_stdalone/app.py +110 -171
- otter_service_stdalone/fs_logging.py +1 -1
- otter_service_stdalone/grade_notebooks.py +103 -0
- otter_service_stdalone/secrets/gh_key.dev.yaml +17 -0
- otter_service_stdalone/secrets/gh_key.local.yaml +16 -0
- otter_service_stdalone/secrets/gh_key.prod.yaml +16 -0
- otter_service_stdalone/secrets/gh_key.staging.yaml +16 -0
- otter_service_stdalone/static_templates/403.html +24 -0
- otter_service_stdalone/static_templates/500.html +24 -0
- otter_service_stdalone/user_auth.py +120 -0
- {otter_service_stdalone-0.1.16.dist-info → otter_service_stdalone-0.1.18.dist-info}/METADATA +1 -1
- otter_service_stdalone-0.1.18.dist-info/RECORD +19 -0
- otter_service_stdalone-0.1.16.dist-info/RECORD +0 -11
- {otter_service_stdalone-0.1.16.dist-info → otter_service_stdalone-0.1.18.dist-info}/WHEEL +0 -0
- {otter_service_stdalone-0.1.16.dist-info → otter_service_stdalone-0.1.18.dist-info}/entry_points.txt +0 -0
- {otter_service_stdalone-0.1.16.dist-info → otter_service_stdalone-0.1.18.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
|
|
1
|
-
__version__ = "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]
|
otter_service_stdalone/app.py
CHANGED
@@ -4,185 +4,130 @@ import tornado.web
|
|
4
4
|
import tornado.auth
|
5
5
|
import os
|
6
6
|
import uuid
|
7
|
-
from otter_service_stdalone import fs_logging as log
|
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
|
8
10
|
from zipfile import ZipFile, ZIP_DEFLATED
|
9
|
-
|
10
|
-
import async_timeout
|
11
|
-
import urllib.parse
|
12
|
-
from otter_service_stdalone import access_sops_keys
|
13
|
-
import json
|
11
|
+
|
14
12
|
|
15
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.
|
16
18
|
|
17
19
|
|
18
|
-
class
|
19
|
-
"""
|
20
|
-
|
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
|
21
25
|
"""
|
22
|
-
async def
|
23
|
-
|
24
|
-
and results of grading generating by otter-grader
|
26
|
+
async def get(self):
|
27
|
+
await u_auth.handle_authorization(self, state)
|
25
28
|
|
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
29
|
|
31
|
-
|
32
|
-
|
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")
|
33
48
|
|
34
|
-
|
35
|
-
|
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
|
49
|
+
|
50
|
+
class GitHubOAuthHandler(BaseHandler):
|
51
|
+
"""Handles GitHubOAuth
|
100
52
|
|
101
53
|
Args:
|
102
54
|
tornado (tornado.web.RequestHandler): The request handler
|
103
55
|
"""
|
104
56
|
async def get(self):
|
105
|
-
# User will be redirected here by GitHub after authorization
|
106
57
|
code = self.get_argument('code', False)
|
107
|
-
|
108
|
-
|
109
|
-
|
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)
|
110
68
|
else:
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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):
|
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):
|
135
84
|
"""The class handling a request to download results
|
136
85
|
|
137
86
|
Args:
|
138
87
|
tornado (tornado.web.RequestHandler): The download request handler
|
139
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
|
140
95
|
async def post(self):
|
141
96
|
"""the post method that accepts the code used to locate the results
|
142
97
|
the user wants to download
|
143
98
|
"""
|
144
|
-
|
145
|
-
directory = f"{__UPLOADS__}/{
|
146
|
-
if
|
147
|
-
|
148
|
-
|
149
|
-
"debug",
|
150
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
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)
|
151
104
|
msg = "Please enter the download code to see your result."
|
152
105
|
self.render("index.html", download_message=msg)
|
153
106
|
elif not os.path.exists(f"{directory}"):
|
154
|
-
|
155
|
-
|
156
|
-
"debug",
|
157
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
107
|
+
m = "Download: Directory for Code Not existing"
|
108
|
+
log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
|
158
109
|
msg = "The download code appears to not be correct or expired "
|
159
|
-
msg += f"- results are deleted regularly: {
|
110
|
+
msg += f"- results are deleted regularly: {download_code}."
|
160
111
|
msg += "Please check the code or upload your notebooks "
|
161
112
|
msg += "and autograder.zip for grading again."
|
162
113
|
self.render("index.html", download_message=msg)
|
163
114
|
elif not os.path.exists(f"{directory}/grading-logs.txt"):
|
164
|
-
|
165
|
-
|
166
|
-
"debug",
|
167
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
115
|
+
m = "Download: Results Not Ready"
|
116
|
+
log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
|
168
117
|
msg = "The results of your download are not ready yet. "
|
169
118
|
msg += "Please check back."
|
170
|
-
self.render("index.html", download_message=msg, dcode=
|
119
|
+
self.render("index.html", download_message=msg, dcode=download_code)
|
171
120
|
else:
|
172
121
|
if not os.path.isfile(f"{directory}/final_grades.csv"):
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
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)
|
177
125
|
with open(f"{directory}/final_grades.csv", "a") as f:
|
178
126
|
m = "There was a problem grading your notebooks. Please see grading-logs.txt"
|
179
127
|
f.write(m)
|
180
128
|
f.close()
|
181
|
-
|
182
|
-
log.write_logs(
|
183
|
-
"",
|
184
|
-
"debug",
|
185
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
129
|
+
m = "Download Success: Creating results.zip"
|
130
|
+
log.write_logs(download_code, m, "", "debug", log_debug)
|
186
131
|
with ZipFile(f"{directory}/results.zip", 'w') as zipF:
|
187
132
|
for file in ["final_grades.csv", "grading-logs.txt"]:
|
188
133
|
if os.path.isfile(f"{directory}/{file}"):
|
@@ -190,7 +135,8 @@ class Download(tornado.web.RequestHandler):
|
|
190
135
|
|
191
136
|
self.set_header('Content-Type', 'application/octet-stream')
|
192
137
|
self.set_header("Content-Description", "File Transfer")
|
193
|
-
|
138
|
+
m = f"attachment; filename=results-{download_code}.zip"
|
139
|
+
self.set_header('Content-Disposition', m)
|
194
140
|
with open(f"{directory}/results.zip", 'rb') as f:
|
195
141
|
try:
|
196
142
|
while True:
|
@@ -203,24 +149,27 @@ class Download(tornado.web.RequestHandler):
|
|
203
149
|
self.write(exc)
|
204
150
|
|
205
151
|
|
206
|
-
class Upload(
|
152
|
+
class Upload(BaseHandler):
|
207
153
|
"""This is the upload handler for users to upload autograder.zip and notebooks
|
208
154
|
|
209
155
|
Args:
|
210
156
|
tornado (tornado.web.RequestHandler): The upload request handler
|
211
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
|
212
164
|
async def post(self):
|
213
165
|
"""this handles the post request and asynchronously launches the grader
|
214
166
|
"""
|
215
|
-
g = GradeNotebooks()
|
167
|
+
g = grade_notebooks.GradeNotebooks()
|
216
168
|
files = self.request.files
|
217
169
|
results_path = str(uuid.uuid4())
|
218
170
|
autograder = self.request.files['autograder'][0] if "autograder" in files else None
|
219
171
|
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')
|
172
|
+
log.write_logs(results_path, "Step 1: Upload accepted", "", "debug", log_debug)
|
224
173
|
if autograder is not None and notebooks is not None:
|
225
174
|
notebooks_fname = notebooks['filename']
|
226
175
|
notebooks_extn = os.path.splitext(notebooks_fname)[1]
|
@@ -232,49 +181,42 @@ class Upload(tornado.web.RequestHandler):
|
|
232
181
|
os.mkdir(__UPLOADS__)
|
233
182
|
auto_p = f"{__UPLOADS__}/{autograder_name}"
|
234
183
|
notebooks_path = f"{__UPLOADS__}/{notebooks_name}"
|
235
|
-
|
236
|
-
|
237
|
-
"debug",
|
238
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
184
|
+
m = "Step 2a: Uploaded File Names Determined"
|
185
|
+
log.write_logs(results_path, m, f"notebooks path: {notebooks_path}", "debug", log_debug)
|
239
186
|
fh = open(auto_p, 'wb')
|
240
187
|
fh.write(autograder['body'])
|
241
188
|
|
242
189
|
fh = open(notebooks_path, 'wb')
|
243
190
|
fh.write(notebooks['body'])
|
244
|
-
|
245
|
-
|
246
|
-
"debug",
|
247
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
191
|
+
m = "Step 3: Uploaded Files Written to Disk"
|
192
|
+
log.write_logs(results_path, m, f"Results Code: {results_path}", "debug", log_debug)
|
248
193
|
m = "Please save this code. You can retrieve your files by submitting this code "
|
249
194
|
m += f"in the \"Results\" section to the right: {results_path}"
|
250
195
|
self.render("index.html", message=m)
|
251
196
|
try:
|
252
197
|
await g.grade(auto_p, notebooks_path, results_path)
|
253
198
|
except Exception as e:
|
254
|
-
log.write_logs(results_path, "Grading Problem",
|
255
|
-
str(e),
|
256
|
-
"error",
|
257
|
-
f'{os.environ.get("ENVIRONMENT")}-logs')
|
199
|
+
log.write_logs(results_path, "Grading Problem", str(e), "error", log_error)
|
258
200
|
else:
|
259
|
-
|
260
|
-
|
261
|
-
"debug",
|
262
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
201
|
+
m = "Step 2b: Uploaded Files not given"
|
202
|
+
log.write_logs(results_path, m, "", "debug", log_debug)
|
263
203
|
m = "It looks like you did not set the notebooks or autograder.zip or both!"
|
264
204
|
self.render("index.html", message=m)
|
265
205
|
|
266
206
|
|
267
207
|
settings = {
|
268
208
|
"cookie_secret": str(uuid.uuid4()),
|
269
|
-
"xsrf_cookies": True
|
209
|
+
"xsrf_cookies": True,
|
210
|
+
"login_url": "/login"
|
270
211
|
}
|
271
212
|
|
272
213
|
application = tornado.web.Application([
|
273
|
-
(r"/",
|
214
|
+
(r"/", MainHandler),
|
215
|
+
(r"/login", LoginHandler),
|
274
216
|
(r"/upload", Upload),
|
275
217
|
(r"/download", Download),
|
276
|
-
(r"/
|
277
|
-
], **settings, debug=
|
218
|
+
(r"/oauth_callback", GitHubOAuthHandler),
|
219
|
+
], **settings, debug=False)
|
278
220
|
|
279
221
|
|
280
222
|
def main():
|
@@ -282,14 +224,11 @@ def main():
|
|
282
224
|
"""
|
283
225
|
try:
|
284
226
|
application.listen(80)
|
285
|
-
|
286
|
-
log.write_logs("Server Start", "Starting Server", "", "info", msg)
|
227
|
+
log.write_logs("Server Start", "Starting Server", "", "info", log_debug)
|
287
228
|
tornado.ioloop.IOLoop.instance().start()
|
288
229
|
except Exception as e:
|
289
|
-
|
290
|
-
|
291
|
-
"error",
|
292
|
-
f'{os.environ.get("ENVIRONMENT")}-debug')
|
230
|
+
m = "Server Starting error"
|
231
|
+
log.write_logs("Server Start Error", m, str(e), "error", log_debug)
|
293
232
|
|
294
233
|
|
295
234
|
if __name__ == "__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
|
@@ -0,0 +1,19 @@
|
|
1
|
+
otter_service_stdalone/__init__.py,sha256=6BiuMUkhwQp6bzUZSF8np8F1NwCltEtK0sPBF__tepU,23
|
2
|
+
otter_service_stdalone/access_sops_keys.py,sha256=nboU5aZ84Elrm5vO0dMgpIF5LLcnecpNAwpxKvj6DvU,2129
|
3
|
+
otter_service_stdalone/app.py,sha256=JrjykDRx1VlxLwZQ_svsM9Pabwr5H8x6AHTI-LUJ_BA,9462
|
4
|
+
otter_service_stdalone/fs_logging.py,sha256=IKFZkc5TmpI6G3vTYFAU9jDjQ-GT5aRxk8kdZ0h0kJE,2390
|
5
|
+
otter_service_stdalone/grade_notebooks.py,sha256=DiIW7oIwq2TROHFiR0A9qqVCoFzd3QhZXxqgWZtCDk8,4498
|
6
|
+
otter_service_stdalone/index.html,sha256=QbSQs31OZhWlCQFE5vvJOlNh-JHEJ3PZPgR4GukzrCA,6032
|
7
|
+
otter_service_stdalone/upload_handle.py,sha256=NB6isuLrLkUCPetUA3ugUlKKpAYw4nvDBVmxpzvgcE8,5157
|
8
|
+
otter_service_stdalone/user_auth.py,sha256=O25EekGSnodWlO6RFftK0_QObbJ4MOQupGJZ4XOeHb0,4616
|
9
|
+
otter_service_stdalone/secrets/gh_key.dev.yaml,sha256=XIIjkGhzA16bJGM3imwL1K6_9xw6-5DuiZ-e73LYA_8,1128
|
10
|
+
otter_service_stdalone/secrets/gh_key.local.yaml,sha256=xMp-IkvntbaDLdao1iDkLPGHrx9-X29t4kC3GnXq03U,1127
|
11
|
+
otter_service_stdalone/secrets/gh_key.prod.yaml,sha256=auPYkkWa3y2p0vEbDG4ZVrah78o-8d9PIjoDF3E6qlI,1127
|
12
|
+
otter_service_stdalone/secrets/gh_key.staging.yaml,sha256=uBgz5UIjxT5XBioDchQQ3X22A1UTZC7mZ_mXbEDTRjM,1127
|
13
|
+
otter_service_stdalone/static_templates/403.html,sha256=K7Jc85v6K8ieRtzBo3JBX__oJCcZ8MEiZcIrhITD5I0,985
|
14
|
+
otter_service_stdalone/static_templates/500.html,sha256=2D0YF8-ocJ9I-hM3P3KdflhggKH52L4bTHhKzd79B9o,944
|
15
|
+
otter_service_stdalone-0.1.18.dist-info/METADATA,sha256=7YDBLYbK4CDJxVKHjayAePGzn_Ncmsm2Ewck-3RrkDI,1345
|
16
|
+
otter_service_stdalone-0.1.18.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
17
|
+
otter_service_stdalone-0.1.18.dist-info/entry_points.txt,sha256=cx447chuIEl8ly9jEoF5-2xNhaKsWcIMDdhUMH_00qQ,75
|
18
|
+
otter_service_stdalone-0.1.18.dist-info/top_level.txt,sha256=6UP22fD4OhbLt23E01v8Kvjn44vPRbyTIg_GqMYL-Ng,23
|
19
|
+
otter_service_stdalone-0.1.18.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
otter_service_stdalone/__init__.py,sha256=yF88-8vL8keLe6gCTumymw0UoMkWkSrJnzLru4zBCLQ,23
|
2
|
-
otter_service_stdalone/access_sops_keys.py,sha256=2f3vszsF6LojCmt3AmInyZvIoGrWnjNlu9mVOXmWvcY,2149
|
3
|
-
otter_service_stdalone/app.py,sha256=do03zXIU_1xeBpiMuC0JhbToNA7zuwSgWc0z4i7UtKc,13011
|
4
|
-
otter_service_stdalone/fs_logging.py,sha256=LTrgadyjQyZ204sS5B1HtchnBV49SKCpfrJdcfuvApo,2417
|
5
|
-
otter_service_stdalone/index.html,sha256=QbSQs31OZhWlCQFE5vvJOlNh-JHEJ3PZPgR4GukzrCA,6032
|
6
|
-
otter_service_stdalone/upload_handle.py,sha256=NB6isuLrLkUCPetUA3ugUlKKpAYw4nvDBVmxpzvgcE8,5157
|
7
|
-
otter_service_stdalone-0.1.16.dist-info/METADATA,sha256=MTcibeq-t56l0XGNF4d_QhptZ_fO2Ys6n8ki_SDSDgM,1345
|
8
|
-
otter_service_stdalone-0.1.16.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
9
|
-
otter_service_stdalone-0.1.16.dist-info/entry_points.txt,sha256=cx447chuIEl8ly9jEoF5-2xNhaKsWcIMDdhUMH_00qQ,75
|
10
|
-
otter_service_stdalone-0.1.16.dist-info/top_level.txt,sha256=6UP22fD4OhbLt23E01v8Kvjn44vPRbyTIg_GqMYL-Ng,23
|
11
|
-
otter_service_stdalone-0.1.16.dist-info/RECORD,,
|
File without changes
|
{otter_service_stdalone-0.1.16.dist-info → otter_service_stdalone-0.1.18.dist-info}/entry_points.txt
RENAMED
File without changes
|
{otter_service_stdalone-0.1.16.dist-info → otter_service_stdalone-0.1.18.dist-info}/top_level.txt
RENAMED
File without changes
|