1#!/usr/bin/env python
2
3from flask import Flask, jsonify, request, Response
4import datetime
5import functools
6import logging
7import os
8import random
9import signal
10import time
11
12__version__ = "0.0.1"
13PORT = int(os.getenv("PORT", "5000"))
14HOSTNAME = os.getenv("HOSTNAME")
15LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG")
16
17app = Flask(__name__)
18
19# Quote storage
20#
21# Obviously, this would more typically involve a persistent backing store. That's not
22# really needed for a demo though.
23
24quotes = [
25 "Abstraction is ever present.",
26 "A late night does not make any sense.",
27 "A principal idea is omnipresent, much like candy.",
28 "Nihilism gambles with lives, happiness, and even destiny itself!",
29 "The light at the end of the tunnel is interdependent on the relatedness of motivation, subcultures, and management.",
30 "Utter nonsense is a storyteller without equal.",
31 "Non-locality is the driver of truth. By summoning, we vibrate.",
32 "A small mercy is nothing at all?",
33 "The last sentence you read is often sensible nonsense.",
34 "668: The Neighbor of the Beast."
35]
36
37# Utilities
38
39
40class RichStatus (object):
41 def __init__(self, ok, **kwargs):
42 self.ok = ok
43 self.info = kwargs
44 self.info['hostname'] = HOSTNAME
45 self.info['time'] = datetime.datetime.now().isoformat()
46 self.info['version'] = __version__
47
48 # Remember that __getattr__ is called only as a last resort if the key
49 # isn't a normal attr.
50 def __getattr__(self, key):
51 return self.info.get(key)
52
53 def __bool__(self):
54 return self.ok
55
56 def __nonzero__(self):
57 return bool(self)
58
59 def __contains__(self, key):
60 return key in self.info
61
62 def __str__(self):
63 attrs = ["%s=%s" % (key, self.info[key])
64 for key in sorted(self.info.keys())]
65 astr = " ".join(attrs)
66
67 if astr:
68 astr = " " + astr
69
70 return "<RichStatus %s%s>" % ("OK" if self else "BAD", astr)
71
72 def toDict(self):
73 d = {'ok': self.ok}
74
75 for key in self.info.keys():
76 d[key] = self.info[key]
77
78 return d
79
80 @classmethod
81 def fromError(self, error, **kwargs):
82 kwargs['error'] = error
83 return RichStatus(False, **kwargs)
84
85 @classmethod
86 def OK(self, **kwargs):
87 return RichStatus(True, **kwargs)
88
89
90template = '''
91<HTML><HEAD><Title>{title}</Title></Head>
92 <BODY>
93 <P><span style="color: {textcolor}">{message}</span><P>
94 </BODY>
95</HTML>
96'''
97
98def standard_handler(f):
99 func_name = getattr(f, '__name__', '<anonymous>')
100
101 @functools.wraps(f)
102 def wrapper(*args, **kwds):
103 rc = RichStatus.fromError("impossible error")
104 session = request.headers.get('x-qotm-session', None)
105 username = request.headers.get('x-authenticated-as', None)
106
107 logging.debug("%s %s: session %s, username %s, handler %s" %
108 (request.method, request.path, session, username, func_name))
109
110 headers_string = ', '.join("{!s}={!r}".format(key, val)
111 for (key, val) in request.headers.items())
112 logging.debug("headers: %s" % (headers_string))
113
114 try:
115 rc = f(*args, **kwds)
116 except Exception as e:
117 logging.exception(e)
118 rc = RichStatus.fromError("%s: %s %s failed: %s" % (
119 func_name, request.method, request.path, e))
120
121 code = 200
122
123 # This, candidly, is a bit of a hack.
124
125 if session:
126 rc.info['session'] = session
127
128 if username:
129 rc.info['username'] = username
130
131 if not rc:
132 if 'status_code' in rc:
133 code = rc.status_code
134 else:
135 code = 500
136
137 if rc.json:
138 resp = jsonify(rc.toDict())
139 resp.status_code = code
140 else:
141 info = {
142 'title': "Quote of the Moment %s" % __version__,
143 'textcolor': 'pink',
144 'message': 'This moment is inadequate for a quote.',
145 }
146
147 if rc:
148 info['textcolor'] = 'black'
149 info['message'] = rc.quote
150 else:
151 info['textcolor'] = 'red'
152 info['message'] = rc.error
153
154 resp = Response(template.format(**info), code)
155
156 if session:
157 resp.headers['x-qotm-session'] = session
158
159 return resp
160
161 return wrapper
162
163# REST endpoints
164
165####
166# GET /health does a basic health check. It always returns a status of 200
167# with an empty body.
168
169
170@app.route("/health", methods=["GET", "HEAD"])
171@standard_handler
172def health():
173 return RichStatus.OK(msg="QotM health check OK")
174
175####
176# GET / returns a random quote as the 'quote' element of a JSON dictionary. It
177# always returns a status of 200.
178
179
180@app.route("/", methods=["GET"])
181@standard_handler
182def statement():
183 return RichStatus.OK(quote=random.choice(quotes),
184 json=request.args.get('json', False))
185
186
187####
188# GET /quote/quoteid returns a specific quote. 'quoteid' is the integer index
189# of the quote in our array above.
190#
191# - If all goes well, it returns a JSON dictionary with the requested quote as
192# the 'quote' element, with status 200.
193# - If something goes wrong, it returns a JSON dictionary with an explanation
194# of what happened as the 'error' element, with status 400.
195#
196# PUT /quote/quotenum updates a specific quote. It requires a JSON dictionary
197# as the PUT body, with the the new quote contained in the 'quote' dictionary
198# element.
199#
200# - If all goes well, it returns the new quote as if you'd requested it using
201# the GET verb for this endpoint.
202# - If something goes wrong, it returns a JSON dictionary with an explanation
203# of what happened as the 'error' element, with status 400.
204
205@app.route("/quote/<idx>", methods=["GET", "PUT"])
206@standard_handler
207def specific_quote(idx):
208 try:
209 idx = int(idx)
210 except ValueError:
211 return RichStatus.fromError("quote IDs must be numbers", status_code=400)
212
213 if (idx < 0) or (idx >= len(quotes)):
214 return RichStatus.fromError("no quote ID %d" % idx, status_code=400)
215
216 if request.method == "PUT":
217 j = request.json
218
219 if (not j) or ('quote' not in j):
220 return RichStatus.fromError("must supply 'quote' via JSON dictionary", status_code=400)
221
222 quotes[idx] = j['quote']
223
224 return RichStatus.OK(quote=quotes[idx], json=request.args.get('json', False))
225
226####
227# POST /quote adds a new quote to our list. It requires a JSON dictionary
228# as the POST body, with the the new quote contained in the 'quote' dictionary
229# element.
230#
231# - If all goes well, it returns a JSON dictionary with the new quote's ID as
232# 'quoteid', and the new quote as 'quote', with a status of 200.
233# - If something goes wrong, it returns a JSON dictionary with an explanation
234# of what happened as the 'error' element, with status 400.
235
236
237@app.route("/quote", methods=["POST"])
238@standard_handler
239def new_quote():
240 j = request.json
241
242 if (not j) or ('quote' not in j):
243 return RichStatus.fromError("must supply 'quote' via JSON dictionary", status_code=400)
244
245 quotes.append(j['quote'])
246
247 idx = len(quotes) - 1
248
249 return RichStatus.OK(quote=quotes[idx], quoteid=idx)
250
251
252@app.route("/crash", methods=["GET"])
253@standard_handler
254def crash():
255 logging.warning("dying in 1 seconds")
256 time.sleep(1)
257 os.kill(os.getpid(), signal.SIGTERM)
258 time.sleep(1)
259 os.kill(os.getpid(), signal.SIGKILL)
260
261# Mainline
262
263
264def main():
265 app.run(debug=True, host="0.0.0.0", port=PORT)
266
267
268if __name__ == "__main__":
269 logging.basicConfig(
270 # filename=logPath,
271 level=LOG_LEVEL, # if appDebug else logging.INFO,
272 format="%%(asctime)s demo-qotm %s %%(levelname)s: %%(message)s" % __version__,
273 datefmt="%Y-%m-%d %H:%M:%S"
274 )
275
276 logging.info("initializing on %s:%d" % (HOSTNAME, PORT))
277 main()
View as plain text