This repository was archived by the owner on Jun 7, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathmain.py
More file actions
191 lines (162 loc) · 6.75 KB
/
main.py
File metadata and controls
191 lines (162 loc) · 6.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# *********************************
# |docname| - Define the BookServer
# *********************************
# :index:`docs to write`: notes on this design. :index:`question`: Why is there an empty module named ``dependencies.py``?
#
# Imports
# =======
# These are listed in the order prescribed by `PEP 8`_.
#
# Standard library
# ----------------
import datetime
import json
import os
import pkg_resources
import traceback
import socket
# Third-party imports
# -------------------
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic.error_wrappers import ValidationError
# Local application imports
# -------------------------
from .applogger import rslogger
from .config import settings
from .crud import create_traceback
from .db import init_models, term_models
from .internal.feedback import init_graders
from .routers import assessment
from .routers import auth
from .routers import books
from .routers import rslogging
from .routers import discuss
from .session import auth_manager
# FastAPI setup
# =============
# _`setting root_path`: see `root_path <root_path>`; this approach comes from `github <https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker/issues/55#issuecomment-879903517>`_.
kwargs = {}
if root_path := os.environ.get("ROOT_PATH"):
kwargs["root_path"] = root_path
app = FastAPI(**kwargs) # type: ignore
rslogger.info(f"Serving books from {settings.book_path}.\n")
# Install the auth_manager as middleware This will make the user
# part of the request ``request.state.user`` `See FastAPI_Login Advanced <https://fastapi-login.readthedocs.io/advanced_usage/>`_
auth_manager.useRequest(app)
# Routing
# -------
#
# .. _included routing:
#
# Included
# ^^^^^^^^
app.include_router(rslogging.router)
app.include_router(books.router)
app.include_router(assessment.router)
app.include_router(auth.router)
app.include_router(discuss.router)
# We can mount various "apps" with mount. Anything that gets to this server with /staticAssets
# will serve staticfiles - StaticFiles class implements the same interface as a FastAPI app.
# See `FastAPI static files <https://fastapi.tiangolo.com/tutorial/static-files/>`_
# maybe we could use this inside the books router but I'm not sure...
# There is so much monkey business with nginx routing of various things with /static/ in the
# path that it is clearer to mount this at something NOT called static
# WARNING this works in a dev build but does not work in production. Need to supply a path to a folder containing the static files. I imagine the same is true for the templates! The build script should use pkg_resources to find the files and copy them.
staticdir = pkg_resources.resource_filename("bookserver", "staticAssets")
app.mount("/staticAssets", StaticFiles(directory=staticdir), name="static")
# Defined here
# ^^^^^^^^^^^^
@app.on_event("startup")
async def startup():
# Check/create paths used by the server.
os.makedirs(settings.book_path, exist_ok=True)
os.makedirs(settings.error_path, exist_ok=True)
assert (
settings.runestone_path.exists()
), f"Runestone application in web2py path {settings.runestone_path} does not exist."
await init_models()
init_graders()
@app.on_event("shutdown")
async def shutdown():
await term_models()
#
# If the user supplies a timezone offset we'll store it in the RS_info cookie
# lots of API calls need this so rather than having each process the cookie
# we'll drop the value into request.state this will make it generally avilable
#
@app.middleware("http")
async def get_session_object(request: Request, call_next):
tz_cookie = request.cookies.get("RS_info")
rslogger.debug(f"In timezone middleware cookie is {tz_cookie}")
if tz_cookie:
try:
vals = json.loads(tz_cookie)
request.state.tz_offset = vals["tz_offset"]
rslogger.info(f"Timzone offset: {request.state.tz_offset}")
except Exception as e:
rslogger.error(f"Failed to parse cookie data {tz_cookie} error was {e}")
response = await call_next(request)
return response
@app.get("/")
def read_root():
return {"Hello": "World"}
class NotAuthenticatedException(Exception):
pass
auth_manager.not_authenticated_exception = NotAuthenticatedException
# Fast API makes it very easy to handle different error types in an
# elegant way through the use of middleware to catch particular
# exception types. The following handles the case where the Dependency
# is not satisfied for a user on an endpoint that requires a login.
@app.exception_handler(NotAuthenticatedException)
def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
"""
Redirect the user to the login page if not logged in
"""
rslogger.debug("User is not logged in, redirecting")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=jsonable_encoder(
{"detail": "You need to be Logged in to access this resource"}
),
)
# If we want to redirect the user to a login page which we really do not...
# return RedirectResponse(url=f"{settings.login_url}")
# See: https://fastapi.tiangolo.com/tutorial/handling-errors/#use-the-requestvalidationerror-body
# for more details on validation errors.
@app.exception_handler(ValidationError)
def level2_validation_handler(request: Request, exc: ValidationError):
"""
Most validation errors are caught immediately, but we do some
secondary validation when populating our xxx_answers tables
this catches those and returns a 422
"""
rslogger.error(exc)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"detail": exc.errors()}),
)
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception):
"""
Most validation errors are caught immediately, but we do some
secondary validation when populating our xxx_answers tables
this catches those and returns a 422
"""
rslogger.error("UNHANDLED ERROR")
rslogger.error(exc)
date = datetime.datetime.utcnow().strftime("%Y_%m_%d-%I.%M.%S_%p")
with open(f"{settings.error_path}/{date}_traceback.txt", "w") as f:
traceback.print_tb(exc.__traceback__, file=f)
f.write(f"Error Message: \n{str(exc)}")
# alternatively lets write the traceback info to the database!
# TODO: get local variable information
# find a way to get the request body without throwing an error on await request.json()
#
await create_traceback(exc, request, socket.gethostname())
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=jsonable_encoder({"detail": exc}),
)