Creating a simple application and server

We'll write a very simple REST server that provides a series of hands of dominoes. This will work by routing a URI to a function that will provide the hands. The function must create a response object that includes one or more of the following items:

  • A status code: The default is 200, which indicates success.
  • Headers: The default is a minimal set of response headers with the content size.
  • Content: This can be a stream of bytes. In many cases, RESTful web services will return a document using JSON notation. The Flask framework provides us with a handy function for converting objects to JSON Notation, jsonify().

A Flask application that deals a simple hand of dominoes can be defined as follows:

from flask import Flask, jsonify, abort
from http import HTTPStatus

app = Flask(__name__)

@app.route("/dominoes/<n>")
def dominoes(n: str) -> Tuple[Dict[str, Any], int]:
try:
hand_size = int(n)
except ValueError:
abort(HTTPStatus.BAD_REQUEST)

if app.env == "development":
random.seed(2)
b = Boneyard(limit=6)
hand_0 = b.deal(hand_size)[0]
app.logger.info("Send %r", hand_0)

return jsonify(status="OK", dominoes=[astuple(d) for d in hand_0]), HTTPStatus.OK

This shows us some of the ingredients in a Flask application. The most essential ingredient is the Flask object itself, assigned to an app variable. In small applications, this is often located in a module that provides a useful __name__ string for the application. In larger applications, a __name__ string may be more helpful than a module name to help identify log messages from the Flask object. If the application is assigned to the app variable, then the automated Flask execution environment will locate it. If we don't call it app, we'll have to provide a function to start the execution.

The @app.route decorator is used for each function that will handle requests and create responses. There are a number of features of the route definition. In this example, we've used the parsing capability. The second item on the path is separated and assigned to the n parameter for the dominoes() function.

Generally, there are four important steps in completing a RESTful transaction:

  • Parsing: The routing did some of the parsing. After the initial path decomposition, the parameter value was checked to be sure that it would lead to valid behavior. In case of a problem, the abort() function is used to return an HTTP status code showing a bad request.
  • Evaluating: This section computes the response. In this case, a fresh set of dominoes is created. The limit of double-six is hard-wired into this application. To make this more useful for other games, the limit should be a configuration value. The deal() method of the Boneyard class creates a list of hands. This function, however, only returns a single hand, so hand_0 is assigned the first of the hands in the list returned by deal().
  • Logging. The Flask logger is used to write a message to a log showing the response. In larger applications, the logging will be more complex. In some cases, there can be multiple logs to provide separate details of authentication or audit history.
  • Responding. The response from a function in Flask can have from one to three items in it. In this case, two values are provided. The jsonify() function is used to return the JSON representation of a dictionary with two keys: "status", and "dominoes". The status code is built from the value of HTTPStatus.OK. Note that each of the Domino objects were converted into a tuple using the dataclasses.astuple() function. These kind of serialization considerations are an important part of REST.

The type hints that Flask functions use are generally very simple. Most functions in a RESTful application will have some combination of the following results:

  • Dict[str, Any]: This is the simple result produced by jsonify(). It will yield the default status of HTTPStatus.OK.
  • Tuple[Dict[str, Any], int]: This is a result with a non-default status code.
  • Tuple[Dict[str, Any], int, Dict[str, str]]: This is a result with headers in addition to a status code and a document.

Other combinations of return values are possible, but are relatively rare. A function that implements a deletion, for example, might return only HTTPStatus.NO_CONTENT to show success without any further details.

We can start a demonstration version of this server from the Bash or Terminal prompt as follows:

$ FLASK_APP=ch13_ex2.py FLASK_ENV=development python -m flask run

This command sets two environment variables, then runs the flask module. The FLASK_APP variable defines which module contains the app object. The FLASK_ENV environment variable sets this as a development server, providing some additional debugging support. This will produce an output similar to the following:

 * Serving Flask app "ch13_ex2.py" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 154-541-338

The Flask app name, ch13_ex2.py, came from the FLASK_APP environment variable. The development environment came from the FLASK_ENV environment variable. debug mode was enabled because the environment was development. We can browse the given URL to interact with those portions of the application that happen to be browser-friendly.

This application responds to GET requests. We can use a browser to perform a request on http://127.0.0.1:5000/dominoes/7 and see a result similar to the following:

{"dominoes": [[2, 0], [5, 5], [5, 2], [5, 0], [0, 0], [6, 3], [2, 1]], "status": "OK"}

In the development mode, the random number generator is seeded with a fixed value, so the value is always as shown in the preceding code. 

The response document shown above contains two features that are typical of RESTful web services:

  • A status item: This is a summary of the response. In some cases, the HTTP status code will provide similar information. In many cases, this will be far more nuanced than the simplistic HTTP status code of 200 OK. 
  • The requested object: In this case, dominoes is a list of two-tuples. This is a representation of the state of a single hand. From this, the original Domino instances can be reconstructed by a client.

The original documents can be rebuilt with the following code:

document = response.get_json()
hand = list(Domino(*d) for d in document['dominoes'])

The client software must include the capability of parsing the JSON from the body of the response. The Domino class definition is used to reproduce objects from the transferred representation.

In the next section, we'll discuss more sophisticated routing and responses.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset