otter-service-stdalone 0.1.15__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.
Files changed (26) hide show
  1. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/PKG-INFO +1 -1
  2. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/setup.cfg +1 -1
  3. otter_service_stdalone-0.1.18/src/otter_service_stdalone/__init__.py +1 -0
  4. otter_service_stdalone-0.1.18/src/otter_service_stdalone/access_sops_keys.py +62 -0
  5. otter_service_stdalone-0.1.18/src/otter_service_stdalone/app.py +235 -0
  6. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone/fs_logging.py +1 -1
  7. otter_service_stdalone-0.1.18/src/otter_service_stdalone/grade_notebooks.py +103 -0
  8. otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.dev.yaml +17 -0
  9. otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.local.yaml +16 -0
  10. otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.prod.yaml +16 -0
  11. otter_service_stdalone-0.1.18/src/otter_service_stdalone/secrets/gh_key.staging.yaml +16 -0
  12. otter_service_stdalone-0.1.18/src/otter_service_stdalone/static_templates/403.html +24 -0
  13. otter_service_stdalone-0.1.18/src/otter_service_stdalone/static_templates/500.html +24 -0
  14. otter_service_stdalone-0.1.18/src/otter_service_stdalone/user_auth.py +120 -0
  15. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/PKG-INFO +2 -2
  16. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/SOURCES.txt +9 -0
  17. otter_service_stdalone-0.1.15/src/otter_service_stdalone/__init__.py +0 -1
  18. otter_service_stdalone-0.1.15/src/otter_service_stdalone/app.py +0 -268
  19. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/README.md +0 -0
  20. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/pyproject.toml +0 -0
  21. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone/index.html +0 -0
  22. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone/upload_handle.py +0 -0
  23. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/dependency_links.txt +0 -0
  24. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/entry_points.txt +0 -0
  25. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/src/otter_service_stdalone.egg-info/top_level.txt +0 -0
  26. {otter_service_stdalone-0.1.15 → otter_service_stdalone-0.1.18}/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: 0.1.15
3
+ Version: 0.1.18
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
26
+ otter_service_stdalone = index.html, secrets/*.yaml, static_templates/*.html
27
27
 
28
28
  [options.entry_points]
29
29
  console_scripts =
@@ -0,0 +1 @@
1
+ __version__ = "0.1.18"
@@ -0,0 +1,62 @@
1
+ import os
2
+ import subprocess
3
+ import yaml
4
+
5
+
6
+ def get(course_key, key_name, sops_path=None, secrets_file=None) -> str:
7
+ """
8
+ This method first tries to decrypt sops file and then tries the
9
+ environment. it will return an Exception if the key is not found
10
+
11
+ :param course_key: the course to find in Google Secrets Manager
12
+ :param key_name: the name of the key to retrieve from the Secrets or the environment
13
+ :param secrets_file: the name of the secrets file associated with the key
14
+ :return: the secret
15
+ """
16
+ try:
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
+ except Exception as ex:
22
+ raise Exception(f"Key not decrypted: {key_name}; please configure: Error: {ex}") from ex
23
+
24
+
25
+ def get_via_sops(course_key, key, sops_path=None, secrets_file=None):
26
+ """
27
+ This function attempts to use sops to decrpyt the secrets/{secrets_file}
28
+
29
+ :param course_key: the course to find in Google Secrets Manager
30
+ :param key: the key to find in the course in Google Secrets Manager
31
+ :param sops_path: [OPTIONAL] used to execute pytests
32
+ :param secrets_file: [OPTIONAL] used to execute pytests
33
+ :return: the value of the key or None
34
+ """
35
+ try:
36
+ if sops_path is None:
37
+ sops_path = "/root/go/bin/sops"
38
+
39
+ secrets_file = secrets_file
40
+
41
+ sops_ot = subprocess.check_output([sops_path, "-d", secrets_file], stderr=subprocess.STDOUT)
42
+ dct = yaml.safe_load(sops_ot)
43
+ if course_key is None:
44
+ return dct[key]
45
+ return dct[course_key][key]
46
+ except Exception as ex:
47
+ raise ex
48
+
49
+
50
+ def get_via_env(course_key, key):
51
+ """
52
+ This checks the environment for the key.
53
+
54
+ :param course_key: the course to find in Google Secrets Manager
55
+ :param key: the key to find in the environment
56
+ :return: the value of the key or None
57
+ """
58
+ if key in os.environ:
59
+ if course_key is None:
60
+ return os.environ[key]
61
+ return os.environ[course_key][key]
62
+ return None
@@ -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") == "otter-stdalone-docker-local-test":
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: otter-service-stdalone
3
- Version: 0.1.15
2
+ Name: otter_service_stdalone
3
+ Version: 0.1.18
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
@@ -2,13 +2,22 @@ README.md
2
2
  pyproject.toml
3
3
  setup.cfg
4
4
  src/otter_service_stdalone/__init__.py
5
+ src/otter_service_stdalone/access_sops_keys.py
5
6
  src/otter_service_stdalone/app.py
6
7
  src/otter_service_stdalone/fs_logging.py
8
+ src/otter_service_stdalone/grade_notebooks.py
7
9
  src/otter_service_stdalone/index.html
8
10
  src/otter_service_stdalone/upload_handle.py
11
+ src/otter_service_stdalone/user_auth.py
9
12
  src/otter_service_stdalone.egg-info/PKG-INFO
10
13
  src/otter_service_stdalone.egg-info/SOURCES.txt
11
14
  src/otter_service_stdalone.egg-info/dependency_links.txt
12
15
  src/otter_service_stdalone.egg-info/entry_points.txt
13
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
14
23
  tests/test_upload_handle.py
@@ -1 +0,0 @@
1
- __version__ = "0.1.15"
@@ -1,268 +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
-
12
- __UPLOADS__ = "/tmp/uploads"
13
-
14
-
15
- class GradeNotebooks():
16
- """The class contains the async grade method for executing
17
- otter grader
18
- """
19
- async def grade(self, p, notebooks_path, results_id):
20
- """Calls otter grade asynchronously and writes the various log files
21
- and results of grading generating by otter-grader
22
-
23
- Args:
24
- p (str): the path to autograder.zip -- the solutions
25
- notebooks_path (str): the path to the folder of notebooks to be graded
26
- results_id (str): used for identifying logs
27
-
28
- Raises:
29
- Exception: Timeout Exception is raised if async takes longer than 20 min
30
-
31
- Returns:
32
- boolean: True is the process completes; otherwise an Exception is thrown
33
- """
34
- try:
35
- notebook_folder = uh.handle_upload(notebooks_path, results_id)
36
- log.write_logs(results_id, "Step 5: Notebook Folder configured for grader",
37
- f"Notebook Folder: {notebook_folder}",
38
- "debug",
39
- f'{os.environ.get("ENVIRONMENT")}-debug')
40
- command = [
41
- 'otter', 'grade',
42
- '-a', p,
43
- '-p', notebook_folder,
44
- "--ext", "ipynb",
45
- "--containers", "10",
46
- "-o", notebook_folder,
47
- "-v"
48
- ]
49
- log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
50
- " ".join(command),
51
- "debug",
52
- f'{os.environ.get("ENVIRONMENT")}-debug')
53
- process = await asyncio.create_subprocess_exec(
54
- *command,
55
- stdin=asyncio.subprocess.PIPE,
56
- stdout=asyncio.subprocess.PIPE,
57
- stderr=asyncio.subprocess.PIPE
58
- )
59
-
60
- # this is waiting for communication back from the process
61
- # some images are quite big and take some time to build the first
62
- # time through - like 20 min for otter-grader
63
- async with async_timeout.timeout(2000):
64
- stdout, stderr = await process.communicate()
65
-
66
- with open(f"{notebook_folder}/grading-output.txt", "w") as f:
67
- for line in stdout.decode().splitlines():
68
- f.write(line + "\n")
69
- log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
70
- f"{notebook_folder}/grading-output.txt",
71
- "debug",
72
- f'{os.environ.get("ENVIRONMENT")}-debug')
73
- with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
74
- for line in stderr.decode().splitlines():
75
- f.write(line + "\n")
76
- log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
77
- f"{notebook_folder}/grading-logs.txt",
78
- "debug",
79
- f'{os.environ.get("ENVIRONMENT")}-debug')
80
- log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
81
- " ".join(command),
82
- "debug",
83
- f'{os.environ.get("ENVIRONMENT")}-debug')
84
- log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
85
- " ".join(command),
86
- "info",
87
- f'{os.environ.get("ENVIRONMENT")}-logs')
88
- return True
89
- except asyncio.TimeoutError:
90
- raise Exception(f'Grading timed out for {notebook_folder}')
91
- except Exception as e:
92
- raise e
93
-
94
-
95
- class Userform(tornado.web.RequestHandler):
96
- """This is the initial landing page for application
97
-
98
- Args:
99
- tornado (tornado.web.RequestHandler): The request handler
100
- """
101
- async def get(self):
102
- """renders index.html on a GET HTTP request
103
- """
104
- self.render("index.html", message=None)
105
-
106
-
107
- class Download(tornado.web.RequestHandler):
108
- """The class handling a request to download results
109
-
110
- Args:
111
- tornado (tornado.web.RequestHandler): The download request handler
112
- """
113
- async def post(self):
114
- """the post method that accepts the code used to locate the results
115
- the user wants to download
116
- """
117
- code = self.get_argument('download')
118
- directory = f"{__UPLOADS__}/{code}"
119
- if code == "":
120
- log.write_logs(code, "Download: Code Not Given!",
121
- f"{code}",
122
- "debug",
123
- f'{os.environ.get("ENVIRONMENT")}-debug')
124
- msg = "Please enter the download code to see your result."
125
- self.render("index.html", download_message=msg)
126
- elif not os.path.exists(f"{directory}"):
127
- log.write_logs(code, "Download: Directory for Code Not existing",
128
- f"{code}",
129
- "debug",
130
- f'{os.environ.get("ENVIRONMENT")}-debug')
131
- msg = "The download code appears to not be correct or expired "
132
- msg += f"- results are deleted regularly: {code}."
133
- msg += "Please check the code or upload your notebooks "
134
- msg += "and autograder.zip for grading again."
135
- self.render("index.html", download_message=msg)
136
- elif not os.path.exists(f"{directory}/grading-logs.txt"):
137
- log.write_logs(code, "Download: Results Not Ready",
138
- f"{code}",
139
- "debug",
140
- f'{os.environ.get("ENVIRONMENT")}-debug')
141
- msg = "The results of your download are not ready yet. "
142
- msg += "Please check back."
143
- self.render("index.html", download_message=msg, dcode=code)
144
- else:
145
- if not os.path.isfile(f"{directory}/final_grades.csv"):
146
- log.write_logs(code, "Download: final_grades.csv does not exist",
147
- "Problem grading notebooks see stack trace",
148
- "debug",
149
- f'{os.environ.get("ENVIRONMENT")}-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
-
155
- log.write_logs(code, "Download Success: Creating results.zip",
156
- "",
157
- "debug",
158
- f'{os.environ.get("ENVIRONMENT")}-debug')
159
- with ZipFile(f"{directory}/results.zip", 'w') as zipF:
160
- for file in ["final_grades.csv", "grading-logs.txt"]:
161
- if os.path.isfile(f"{directory}/{file}"):
162
- zipF.write(f"{directory}/{file}", file, compress_type=ZIP_DEFLATED)
163
-
164
- self.set_header('Content-Type', 'application/octet-stream')
165
- self.set_header("Content-Description", "File Transfer")
166
- self.set_header('Content-Disposition', f"attachment; filename=results-{code}.zip")
167
- with open(f"{directory}/results.zip", 'rb') as f:
168
- try:
169
- while True:
170
- data = f.read(4096)
171
- if not data:
172
- break
173
- self.write(data)
174
- self.finish()
175
- except Exception as exc:
176
- self.write(exc)
177
-
178
-
179
- class Upload(tornado.web.RequestHandler):
180
- """This is the upload handler for users to upload autograder.zip and notebooks
181
-
182
- Args:
183
- tornado (tornado.web.RequestHandler): The upload request handler
184
- """
185
- async def post(self):
186
- """this handles the post request and asynchronously launches the grader
187
- """
188
- g = GradeNotebooks()
189
- files = self.request.files
190
- results_path = str(uuid.uuid4())
191
- autograder = self.request.files['autograder'][0] if "autograder" in files else None
192
- notebooks = self.request.files['notebooks'][0] if "notebooks" in files else None
193
- log.write_logs(results_path, "Step 1: Upload accepted",
194
- "",
195
- "debug",
196
- f'{os.environ.get("ENVIRONMENT")}-debug')
197
- if autograder is not None and notebooks is not None:
198
- notebooks_fname = notebooks['filename']
199
- notebooks_extn = os.path.splitext(notebooks_fname)[1]
200
- notebooks_name = results_path + notebooks_extn
201
- autograder_fname = autograder['filename']
202
- autograder_extn = os.path.splitext(autograder_fname)[1]
203
- autograder_name = str(uuid.uuid4()) + autograder_extn
204
- if not os.path.exists(__UPLOADS__):
205
- os.mkdir(__UPLOADS__)
206
- auto_p = f"{__UPLOADS__}/{autograder_name}"
207
- notebooks_path = f"{__UPLOADS__}/{notebooks_name}"
208
- log.write_logs(results_path, "Step 2a: Uploaded File Names Determined",
209
- f"notebooks path: {notebooks_path}",
210
- "debug",
211
- f'{os.environ.get("ENVIRONMENT")}-debug')
212
- fh = open(auto_p, 'wb')
213
- fh.write(autograder['body'])
214
-
215
- fh = open(notebooks_path, 'wb')
216
- fh.write(notebooks['body'])
217
- log.write_logs(results_path, "Step 3: Uploaded Files Written to Disk",
218
- f"Results Code: {results_path}",
219
- "debug",
220
- f'{os.environ.get("ENVIRONMENT")}-debug')
221
- m = "Please save this code. You can retrieve your files by submitting this code "
222
- m += f"in the \"Results\" section to the right: {results_path}"
223
- self.render("index.html", message=m)
224
- try:
225
- await g.grade(auto_p, notebooks_path, results_path)
226
- except Exception as e:
227
- log.write_logs(results_path, "Grading Problem",
228
- str(e),
229
- "error",
230
- f'{os.environ.get("ENVIRONMENT")}-logs')
231
- else:
232
- log.write_logs(results_path, "Step 2b: Uploaded Files not given",
233
- "",
234
- "debug",
235
- f'{os.environ.get("ENVIRONMENT")}-debug')
236
- m = "It looks like you did not set the notebooks or autograder.zip or both!"
237
- self.render("index.html", message=m)
238
-
239
-
240
- settings = {
241
- "cookie_secret": str(uuid.uuid4()),
242
- "xsrf_cookies": True
243
- }
244
-
245
- application = tornado.web.Application([
246
- (r"/", Userform),
247
- (r"/upload", Upload),
248
- (r"/download", Download),
249
- ], **settings, debug=True)
250
-
251
-
252
- def main():
253
- """the web servers entry point
254
- """
255
- try:
256
- application.listen(80)
257
- msg = f'{os.environ.get("ENVIRONMENT")}-debug'
258
- log.write_logs("Server Start", "Starting Server", "", "info", msg)
259
- tornado.ioloop.IOLoop.instance().start()
260
- except Exception as e:
261
- log.write_logs("Server Start Error", "Server Starting error",
262
- str(e),
263
- "error",
264
- f'{os.environ.get("ENVIRONMENT")}-debug')
265
-
266
-
267
- if __name__ == "__main__":
268
- main()