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.
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.
1 from banjo.urls import route_get, route_post
2 from banjo.http import BadRequest
3 from app.models import Riddle
4
5 @route_get('all', args={})
6 def 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})
11 def 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})
21 def 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})
29 def 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:
- 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) - 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, thelist_riddles
function is called. (Try changing "all" to another string and then re-runbanjo
; you will see that the route has changed. - 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 asparams
. For example, line 20 specifies that when callingshow
,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.
BadRequest
(400)Forbidden
(403)NotFound
(404)NotAllowed
(405)
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:
Riddle.objects.all()
gets all existing riddles from the database (line 7)riddle = Riddle.from_dict(params)
creates a new riddle (line 12)riddle.validate()
checks whether a riddle's properties are ok (line 13)riddle.save()
saves a riddle into the database (line 15)riddle.to_dict()
represents a riddle as a dict (line 16)Riddle.objects.get(id=1)
fetches one specific riddle (line 23)riddle.check_guess("answer?")
checks whether a guess is the answer to the riddle (line 32)
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:
1 from banjo.models import Model, StringField, IntegerField
2 from fuzzywuzzy import fuzz
3
4 class 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"
}
URL | Params | Description |
---|---|---|
/lines/random | Returns a random line. | |
/lines/about | topic (str ) | Returns a random line containing topic |
/lines/rhyme | word (str ) | Returns a random line which rhymes with word |
/couplets/random | Returns a random rhyming couplet. | |
/couplets/about | topic (str ) | Returns a random rhyming couplet where the first line contains topic . |
/couplets/rhyme | word (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:
- Open the app's API page at http://localhost:5000/api. From here you can test all the routes.
- You can use your browser to test individual routes by clicking the links in the table above. When there is a param, you can edit it in the browser's URL bar.
- You can use
http
from the command line, for example:http get http://localhost:5000/lines/random
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.