Server lab

In the Riddles Lab, you interacted with a riddle server running at https://riddles.makingwithcode.org, using the command line and also using a client program. In this project you will get to know the code running on the riddle server, and will build your own server.

The Riddle Server

Navigate to this project's directory, entry a Poetry shell, and open riddle_server in your code editor (code riddle_server) You should see a single directory, app, with two files inside: models.py and views.py. These two files, which together have under 100 lines of code, implement the riddle server. How is that possible? The riddle server uses a library called Banjo, which provides powerful classes and functions which do almost all of the work for you.

Banjo is built on top of another library called Django...

Django is the most popular Python framework for writing web applications--programs which act as servers on the Internet, receiving requests and returning responses. (Instagram is written in Django.) One of the best parts of Django is its comprehensive and well-written documentation. If this course has been your first introduction to programming, you're probably not quite ready to learn Django, but you might be able to do it with a little help from some YouTube videos.

Chris wrote Banjo, which is a simplified wrapper around Django. Not as powerful, but much easier to learn. Once you have a little experience with Banjo, learning Django will be easier. Creating simplified wrappers over full-power tools can be an effective pedagogical strategy.

💻 Run the riddle server on your own computer. From the riddle_server directory, run banjo. You'll see a bunch of setup run, and then

Starting development server at http://127.0.0.1:5000/
Quit the server with CONTROL-C.

Note the URL on the second-to-last line: http://127.0.0.1:5000. Instead of a domain name (like riddles.makingwithcode.org), this URL is an IP address. Every computer on the Internet has an IP address; a computer will receive messages addressed to its IP address. 127.0.0.1 is a special IP address which always refers to "this computer." 5000 is the port number: processes running on a computer can listen on a specific port, and then will receive messages addressed to that port. Default port numbers are assigned for http, email, ftp, and other kinds of network traffic.

💻 Open a web browser and enter http://127.0.0.1:5000/api (localhost:5000/api will work too.) You should see the riddle server API, a page which docmuentats how to interact with the riddle server. If you click on the various routes, you can try out each one from the browser. These routes should be familiar from the previous lab.

Screen shot of the riddle server API

After you have interacted with your own local instance of the riddle server, switch back to Terminal. You'll see a log of every request.

[27/Mar/2024 22:30:33] "GET /api/ HTTP/1.1" 404 179
[27/Mar/2024 22:30:35] "GET /api HTTP/1.1" 200 2193
[27/Mar/2024 22:30:39] "GET /api/all HTTP/1.1" 200 1553
[27/Mar/2024 22:30:41] "GET /api HTTP/1.1" 200 2193
[27/Mar/2024 22:30:43] "GET /api/new HTTP/1.1" 200 1854
[27/Mar/2024 22:37:52] "POST /api/new HTTP/1.1" 200 2080
[27/Mar/2024 22:37:57] "GET /api HTTP/1.1" 200 2193
[27/Mar/2024 22:37:59] "GET /api/guess HTTP/1.1" 200 1835
[27/Mar/2024 22:38:05] "POST /api/guess HTTP/1.1" 200 2198

Press Control + C to stop the server. (If you try to reload the page in your web browser, it won't work anymore because nobody's home...)

Views

In the Games Unit, we used a common Object-Oriented Programming pattern, dividing the program into a model (which enforces the game rules and keeps track of state) and a view (which handles interactions with the player). Web applications often use a similar pattern. The model implements the core logic of the app and stores data; the view takes care of receiving requests and returning responses.

💻 Open riddle_server/app/views.py, where these routes are defined.

1from banjo.urls import route_get, route_post
2from banjo.http import BadRequest
3from app.models import Riddle
4
5@route_get('all', args={})
6def list_riddles(params):
7 riddles = sorted(Riddle.objects.all(), key=lambda riddle: riddle.difficulty())
8 return {'riddles': [riddle.to_dict(with_answer=False) for riddle in riddles]}
9
10@route_post('new', args={'question': str, 'answer': str})
11def create_riddle(params):
12 riddle = Riddle.from_dict(params)
13 errors = riddle.validate()
14 if len(errors) == 0:
15 riddle.save()
16 return riddle.to_dict(with_answer=False)
17 else:
18 raise BadRequest("Riddle not found")
19
20@route_get('show', args={'id': int})
21def show_riddle(params):
22 try:
23 riddle = Riddle.objects.get(id=params['id'])
24 return riddle.to_dict(with_answer=False)
25 except Riddle.DoesNotExist:
26 raise BadRequest("Riddle not found")
27
28@route_post('guess', args={'id': int, "answer": str})
29def guess_answer(params):
30 try:
31 riddle = Riddle.objects.get(id=params['id'])
32 correct = riddle.check_guess(params['answer'])
33 return {
34 "guess": params['answer'],
35 "correct": correct,
36 "riddle": riddle.to_dict(with_answer=correct)
37 }
38 except Riddle.DoesNotExist:
39 raise BadRequest("Riddle not found")

Give this file a skim. There are a few new constructs to discuss, but hopefully you can make some sense of what's here. Four functions are defined, list_riddles, create_riddle, show_riddle, and guess_answer. Each function receives a dict called params and returns another dict. Each of these functions implements one API endpoint of the Riddle Server. Here are a few new constructs:

Decorators

Lines 5, 10, 20, and 28 have @route_get or @route_post preceding function definitions. These functions prefixed with @ are called decorators, and they modify functions. In this context, the @route_get decorator does three things:

  1. Registers this function as a route in the app that should receive HTTP GET requests. (route_post works the same, except for HTTP POST requests)
  2. The first argument to route_get names the URL route by which this function will be accessed. For example, the decorator on line 5 declares the route "all". When you go to https://riddles.makingwithcode.org/all, the list_riddles function is called. (Try changing "all" to another string and then re-run banjo; you will see that the route has changed.
  3. The second argument to route_get specifies the arguments which should be provided, and their types. When a request is received, Banjo will automatically check that the correct arguments are present, and that they are of the correct type. If there are no errors, the arguments dictionary is passed to the function as params. For example, line 20 specifies that when calling show, id must be present (naming which riddle you want), and it must be an integer. Compare the result of these two URLs:

Exceptions

The other construct you have probably not seen before is the use of try and except to handle exceptions. By this point, you're probably very familiar with crashing programs. For example, if you run 1 / 0 in a Python program, the program will raise a ZeroDivisionError and crash. Usually this is the right thing to do: when something goes wrong, the program should exit.
However, you can also write programs which anticipate and recover from errors. If an error is raised within a try block, execution of code within the block stops. Then, if any of the following except blocks matches the exception, that block runs and the program continues. If none of the except blocks matches, the error is unhandled and the program crashes.

Banjo defines four exceptions which correspond to HTTP response codes.

If any of these exceptions is raised within a function, Banjo will handle it and return a response with the corresponding error code. For example, create_riddle (lines 10-18) creates a riddle based on the params provided in the request. The validate method checks whether there are any problems with the riddle. If so, BadRequest is raised, which causes Banjo to return a response with status code 400, telling the user they did something wrong.

A slightly different pattern is used in show_riddle (lines 21-26). Line 23 tries to look up an existing riddle using the riddle ID provided in the request. If no such riddle exists, the Riddle.DoesNotExist exception is raised. In this case, the Riddle.DoesNotExist exception is handled and NotFound is raised instead (lines 25-26), which causes Banjo to respond with the appropriate error code.

Models

The last part of views.py that needs an explanation is Riddle, the class imoported from app.models (line 3). Riddle has some really useful methods:

A few of these methods were written for this app, but most come for free from Banjo. How does that work? Open up riddle_server/app/models.py in your code editor. Let's focus on the first few lines:

1from banjo.models import Model, StringField, IntegerField
2from fuzzywuzzy import fuzz
3
4class Riddle(Model):
5 question = StringField()
6 answer = StringField()
7 guesses = IntegerField()
8 correct = IntegerField()

Subclasses

There is a lot going on in models.py, but there's only one construct you haven't seen before. When the Riddle class is defined, it is defined as a subclass of Model, which was imported from Banjo.

Instead of starting off as a blank slate, Riddle starts off will all the methods and properties of Model. All the methods we define in Riddle are just added to what it already inherited from Model. (If we define a method with the same name as one it already had, the new one overrides the old one.) This is a very common pattern: frameworks can write powerful classes with lots of functionality, then you can subclass them and modify them so they work the way you want. The downside is that a class like Riddle now has lots of secret powers that aren't visible. That's where it's important to read the documentation: here's the documentation for Banjo, which explains how Banjo models work.

Model fields

Lines 5-8 declare the fields which belong to a Riddle: each riddle has a question, an answer, a number of guesses, and a number of correct guesses. One of the capabilities Riddle inherits from Model is that each riddle can be saved into a database, and then riddles can be fetched from the database later, while handling another request. At the top of this section, we pointed out the code used to save riddles and then to look them up later.

Poems app

Now that you have gotten to know the riddle server, it's your turn to build out an app for poetry. Three models are provided (Poem, Line, and Rhyme); your job is to write the views.

💻 Navigate to the poem_app directory. Run python import_poems.py --limit 100000 to load some lines of poetry into the database. (If you don't use the --limit flag, 3 million lines of poetry from Project Guenberg will be imported; this takes a while!)

Now let's try out the models. Enter Banjo's interactive mode by running banjo --shell. Try out each of the following examples.

Count the number of poems

>>> Poem.objects.count()
23

Count the number of lines

>>> Line.objects.count()
99969

Get a random line of poetry

>>> Line.objects.random()
And several bookies were killed in the crush.

Get a random sample of lines

>>> for line in Line.objects.sample(3):
...     print(line)
... 
At his approach they toss their heads on high,
The unpitying fates recall me, and dark sleep
Men of the High North, the wild sky is blazing;

Get lines which contain a string

>>> Line.objects.filter(clean_text__contains="elephant").random()
And I like the smell of the trampled grass and elephants and hay.

Get lines which start with a string

>>> Line.objects.filter(clean_text__startswith="some day").random()
Some day I shall rise and leave my friends

Get lines which rhyme with a line

>>> line = Line.objects.random()
>>> line
A land there is, Hesperia call'd of old,
>>> line.rhyming_lines().count()
131
>>> for rl in line.rhyming_lines().sample(3):
...     print(rl)
...
Sit unpolluted, and th' ethereal mould,
That I not whos opinion I may holde.
That to the deeth myn herte is to hir holde.

Get lines which rhyme with a word

>>> Rhyme.get_rhyme_for_word("man").lines.exclude(clean_text__endswith=" man").first()
A little dog danced. And the day began.

API Specification

Your task is to write views so that the following routes work in the app. If the request cannot be fulfilled (e.g. because there is no word which rhymes), banjo.http.NotFound or banjo.http.BadRequest should be raised with an error message to return status code 404 or 400 respectively.

When a route returns a single line, the response should be formatted like {"line": "A line of poetry"}. When a route returns multiple lines, the response should be formatted like {"lines": ["First line", "Second line"]}. When a route requires a param, that param key and value should also be included in the response. For example, /couplets/about?topic=apple might return:

{
 "lines": [
  "High up in the apple tree climbing I go,",
  "I have ben ofte moeved so,"
 ],
 "topic": "apple"
}
URLParamsDescription
/lines/randomReturns a random line.
/lines/abouttopic (str)Returns a random line containing topic
/lines/rhymeword (str)Returns a random line which rhymes with word
/couplets/randomReturns a random rhyming couplet.
/couplets/abouttopic (str)Returns a random rhyming couplet where the first line contains topic.
/couplets/rhymeword (str)Returns a random rhyming couplet where both lines rhyme with word.
Your choice!?Implement at least one additional route of your own design.

How to work

All the code you need to write should go into poem_server/app/views.py. After you add a new route, start the server by running banjo (from poem_server) to test out the route. There are a few different ways to test out your app:

How to debug

If your server is crashing (500 error), you may want to run banjo --debug instead; when the app crashes in debug mode, it returns a nicely-formatted webpage full of information which can help you figure out what went wrong. Of course, you can also print out variable values within your views to figure out what's going on. And as always, you are not alone--ask for help from peers or from a teacher.

Build your own app