-
Notifications
You must be signed in to change notification settings - Fork 0
/
server.py
184 lines (147 loc) · 5.82 KB
/
server.py
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
from typing import Dict, Iterable, Union
from flask import Flask, request
from google.cloud import datastore
import datetime
from job_verifier.job_verifier import verify_all
from job_processor.job_processor import run as run_jobs
from job_processor.collect_results import collect_results
import os
import json
PROJECT_ID = os.environ['GOOGLE_CLOUD_PROJECT']
PROCESSOR_ID = os.environ['GOOGLE_CLOUD_PROCESSOR']
# Connect to datastore
client = datastore.Client(project=PROJECT_ID)
# Initialize flask
app = Flask(__name__)
# available fields and their defaults. None if the field is required
fields = {'circuit':None, 'email':None, 'repetitions':None, 'student_id':None, 'note':'Your Note Here'}
def store_job(data: Dict[str,str], client: datastore.Client) -> int:
""" Stores job datastore
Args:
data: dict of HTML fields
client: datastore client to save to
Returns:
key that job was stored with
"""
# initialize entity
key = client.key('job')
entity = datastore.Entity(key=key, exclude_from_indexes=['note', 'circuit', 'repetitions'])
# fill entity fields
for field, default in fields.items():
value = data.get(field)
entity[field] = value if value else default
entity['submission_timestamp'] = datetime.datetime.utcnow()
entity['submission_version'] = os.environ.get('GAE_VERSION')
entity['verified'] = False
entity['done'] = False
entity['sent'] = False
# store entity
client.put(entity)
# return entity key
return entity.key
# TODO need type hint help on this one
def fetch_by_job(job_ids: Iterable[int], client: datastore.Client) -> Dict[int, Dict]:
""" Fetches jobs from datastore by job id
Arg:
job_ids: Iterable of job identifiers
client: datastore client for retrieving from
Returns:
dict of job result dictionaries, by job id
"""
# collect job id keys
keys = [client.key('job', int(job_id)) for job_id in job_ids]
# query for all entities simultaneously
entities = client.get_multi(keys)
# index entities by their identifier, turn into json-able dict, and return
results = {entity.id: entity for entity in entities}
return results
def fetch_by_student(student_ids: Iterable[int], client: datastore.Client) -> Dict[int, Dict]:
""" Fetches jobs from datastore by student id
Arg:
student_ids: Iterable of student identifiers
client: datastore client for retrieving from
Returns:
dict of job result dictionaries, by student id
"""
# results dict, by student id
results = {}
for student_id in student_ids:
# build query
query = client.query(kind="job")
query.add_filter("student_id", "=", int(student_id))
# query for entities
entities = query.fetch()
results[student_id] = {entity.id: entity for entity in entities}
return results
@app.route('/lookup', methods=['GET'])
def lookup() -> Dict[int, Dict]:
""" Looks up job result(s) in datastore from request job_id(s) in HTML payload
Returns:
generator of response messages, including job results or error messages
"""
# check that request has args attached
if request.args:
# check that either 'job_id' or 'student_id' is one of the args
if not {'job_id', 'student_id'}.intersection(request.args):
return "Request GET should contain 'job_id' or 'student_id' argument fields"
response = {}
# yield jobs by job id
try:
if 'job_id' in request.args:
response['Jobs by job_id'] = fetch_by_job(request.args.getlist('job_id'), client)
except Exception as e:
return "Exception:" + str(e)
# yield jobs by student id
try:
if 'student_id' in request.args:
response['Jobs by student_id'] = fetch_by_student(request.args.getlist('student_id'), client)
except Exception as e:
return "Exception:" + str(e)
return json.dumps(response, indent=4, default=str)
else:
return "Please GET with a 'job_id' argument field"
@app.route('/send', methods=['GET', 'POST'])
def send() -> str:
""" Checks HTML payload for correct fields and stores job
Returns:
string response message
"""
failure_string = 'Please POST a json object containing the following fields:\n' + \
', '.join(str(field) + ('(optional)' if default else '') for field,default in fields.items())
# check that request is of type POST and has a json attached
if request.method == 'POST' and request.json:
# check that json has required fields
required_fields = {field for field,default in fields.items() if not default}
if any(x not in request.json for x in required_fields):
return failure_string
# store job from json
job_id = store_job(request.json, client).id
# return job id
return "Job Stored with ID: " + str(job_id)
else:
return failure_string
@app.route('/verify', methods=['GET'])
def verify():
if not request.headers.get('X-Appengine-Cron'):
return "Not Authorized", 401
return verify_all(PROJECT_ID, PROCESSOR_ID)
@app.route('/run', methods=['GET'])
def run():
if not request.headers.get('X-Appengine-Cron'):
return "Not Authorized", 401
return run_jobs(PROJECT_ID, PROCESSOR_ID)
@app.route('/collect', methods=['GET'])
def collect():
if not request.headers.get('X-Appengine-Cron'):
return "Not Authorized", 401
return collect_results(PROJECT_ID, PROCESSOR_ID)
@app.route('/')
def root() -> str:
""" Default root page
Returns:
string response message
"""
return "Use /send or /lookup"
if __name__ == '__main__':
# run as local server for testing
app.run(host='127.0.0.1', port=8080, debug=True)