...

Text file src/github.com/datawire/ambassador/v2/demo/services/qotm.py

Documentation: github.com/datawire/ambassador/v2/demo/services

     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