-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathserver.py
More file actions
258 lines (211 loc) · 8.26 KB
/
server.py
File metadata and controls
258 lines (211 loc) · 8.26 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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""
This module contains functions for receiving user input and formatting
returned data.
Retrieval logic of data is done in access.py.
"""
# 3rd party
from flask import Flask, jsonify, request, render_template
# Owl modules
# The reason for the relatively verbose imports is to avoid names
# like 'model', 'request' or 'filter' being in the module namespace,
# which may easily be accidentally overridden or mistaken for variables
# of other types.
import settings
import owl.model # Bring in all objects from model without an ambiguous name.
import owl.input
import owl.access
import owl.filter
# Quart config
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*'
return response
application = Flask(
__name__,
template_folder="../frontend/templates",
static_folder='../frontend/static'
)
application.after_request(add_cors_headers)
CAMPUS_LIST = {'fh', 'da'}
# fields
SCHOOL_KEY = 'school'
DEPARTMENT_KEY = 'dept'
COURSE_KEY = 'course'
QUARTER_KEY = 'quarter'
SECTION_KEY = 'section'
FILTER_KEY = 'filters'
FILTER_CONFLICTS_KEY = 'conflict_sections'
data_model = owl.model.DataModel(settings.DB_DIR)
accessor = owl.access.ModelAccessor(data_model)
@application.route('/')
def idx():
return render_template('index.html')
def basic_checks(input_type=None):
"""
Wrapper for an api call that handles common exception cases.
:param input_type:
:return: Callable
"""
def decorator(f):
def api_method_wrapper(*args, **kwargs):
if input_type:
inputs = input_type(request)
if not inputs.validate():
return jsonify(success=False, errors=inputs.errors)
try:
response = f(*args, **kwargs)
except owl.access.AccessException as e:
return e.user_msg, 404 # Data not found
else:
return response
api_method_wrapper.__name__ = f.__name__ + '_wrapper'
return api_method_wrapper
return decorator
@application.route('/<campus>/single', methods=['GET'])
@basic_checks(input_type=owl.input.GetOneInput)
def api_one(campus: str):
"""
`/single` with [GET] handles a single request to get a whole
department or a whole course listing from the database
It expects a mandatory query parameter `dept` and an
optional `course`.
Example: {'dept': 'CS', 'course': '2C'}
If only `dept` is requested, it checked for its existence in the
database and then returns it.
However, if `course` is also selected, it will return only the data
of that course within the department.
:param campus: (str) Campus to retrieve data from.
:return: 200 - Found entry and returned data successfully to
the user.
:return: 400 - Badly formatted request.
:return: 404 - Could not find entry.
"""
# 'campus' refers here to the school, not the physical location,
# and is not the same thing as the 'campus' field in section data.
# This is something that should probably be refactored if possible.
data = accessor.get_one(
school=campus.upper(),
quarter=request.args.get(QUARTER_KEY, owl.access.LATEST),
department=request.args[DEPARTMENT_KEY], # No default; required field.
course=request.args.get(COURSE_KEY, owl.access.ALL),
section_filter=_get_section_filter(request.args), # May return None.
)
return jsonify(data), 200
@application.route('/<campus>/batch', methods=['POST'])
@basic_checks(input_type=owl.input.GetManyInput)
def api_many(campus):
"""
`/batch` with [POST] handles a batch request to get many
departments or a many course listings from the database.
This batch request is meant to simulate hitting the api route with
this data N times. It expects a mandatory list of objects
containing keys `dept` and `course`.
`/batch` also accepts a series of filters that can be applied to
the results. See filter_courses() for info about what each
filter does.
Example Body:
{
'courses': [
{'dept': 'MATH', 'course': '1A'},
{'dept': 'ENGL', 'course': '1A'},
{'dept': 'CHEM', 'course': '1A'}
],
'filters': {
'days': {'M':1, 'T':0, 'W':1, 'Th':0, 'F':0, 'S':0, 'U':0},
'types': {'standard':1, 'online':1, 'hybrid':0},
'status': {'open':1, 'waitlist':0, 'full':0},
'time': {'start':'8:30 AM', 'end':'9:40 PM'}
}
}
:param campus: (str) Campus to retrieve data from
:return: 200 - Found all entries and returned data successfully
to the user.
:return: 404 - Could not find one or more entries.
"""
course_filter = _get_section_filter(request.args)
def get_sub_request_data(args):
try:
return accessor.get_one(
school=campus,
department=args[DEPARTMENT_KEY],
course=args.get(COURSE_KEY, owl.access.ALL),
quarter=args.get(QUARTER_KEY, owl.access.LATEST),
section_filter=_get_section_filter(args) or course_filter
)
except owl.access.AccessException:
return {}
data = list(map(get_sub_request_data, request.args['courses']))
if any(not sub_data for sub_data in data):
response_code = 404
else:
response_code = 200
json = jsonify({'courses': data})
return json, response_code
@application.route('/<campus>/list', methods=['GET'])
@basic_checks(input_type=owl.input.GetListInput)
def api_list(campus):
"""
`/list` with [GET] handles a single request to list department or
course keys from the database
It takes an optional query parameter `dept` which is first checked
for existence and then returns the dept keys.
However, if `course` is also selected, it will return only the data
of that course within the department.
:param campus: (str) The campus to retrieve data from
:return: 200 - Found entry and returned keys successfully
to the user.
:return: 404 - Could not find entry
"""
if campus not in CAMPUS_LIST:
return 'Error! Could not find campus in database', 404
data = accessor.get_one(
school=campus.upper(),
quarter=request.args.get(QUARTER_KEY, owl.access.LATEST),
department=request.args[DEPARTMENT_KEY], # No default; required field.
course=request.args.get(COURSE_KEY, owl.access.ALL),
section_filter=_get_section_filter(request.args), # May return None.
).keys()
return ('Error! Could not list', 404) if not data else (jsonify(data), 200)
@application.route('/<campus>/urls', methods=['GET'])
@basic_checks(input_type=owl.input.GetUrlsInput)
def api_list_url(campus):
"""
`/urls` with [GET] returns a tree of all departments in a quarter,
their courses, and the courses' endpoints to hit.
:param campus: (str) The campus to retrieve data from
:return: 200 - Should always return
"""
if campus not in CAMPUS_LIST:
return 'Error! Could not find campus in database', 404
data = accessor.get_urls(
school=campus,
quarter=request.args.get(QUARTER_KEY, owl.access.LATEST)
)
return jsonify(data), 200
def _get_section_filter(args):
"""
Produces a SectionFilter from passed arguments
:param args: request args
:return: SectionFilter or None
"""
if not args[FILTER_KEY]:
return None
filter_kwargs = args.copy()
try:
conflict_sections = filter_kwargs[FILTER_CONFLICTS_KEY]
except KeyError:
pass
else:
# replace filter conflicts identifiers with set of
# section views.
filter_kwargs[FILTER_CONFLICTS_KEY] = {
accessor.get_section(
school=section_identifiers[SCHOOL_KEY],
quarter=section_identifiers[QUARTER_KEY],
department=section_identifiers[DEPARTMENT_KEY],
course=section_identifiers[COURSE_KEY],
section=section_identifiers[SECTION_KEY]
) for section_identifiers in conflict_sections
}
return owl.filter.SectionFilter(**filter_kwargs)
if __name__ == '__main__':
application.run(host='0.0.0.0', debug=True, threaded=True)