Our web application has two required external libraries, flask
, and requests
. As our list of dependencies becomes more complicated, we want to list them in a file called requirements.txt
and include it with our project. That way, our code can be reused by others.
Open and look at the requirements.txt
file. The name of each dependency is on a new line.
As you advance in your Python journey, you can use the more advanced pipenv
tool to handle complicated dependencies.
To install all the dependencies from our requirements file, pass the -r
flag to pip
, and the name of the file (in this case, it’s requirements.txt
):
(env)$ python -m pip install -r requirements.txt
Let’s review what we learned over the last two days and put it all together.
For our final exercise today, we’re going to build on yesterday’s final exercise, where we wrote a program to query the GitHub API for a list of repos for certain programming languages, sorted by number of stars. We’ll be turning yesterday’s exercise into a Flask webapp. Flask is a simple and popular framework for creating basic web apps in Python.
First, create a new folder for this exercise, called day_two_final
. You’ll need two folders of static content - CSS and HTML files - to make this work. You can download them here. Unzip your static_files.zip
file and copy your static
and template
folders to your day_two_final
folder.
Next, create a folder called repos
. This is where we’ll create our custom module. Inside this folder we’ll create three files: exceptions.py
, models.py
, and api.py
.
In exceptions.py
, we’ll create a custom exception class to handle errors with the GitHub API.
In models.py
, we’ll create a GitHubRepo
class to more easily represent the results from the GitHub API search.
And api.py
will hold our functions for querying the GitHub API.
Finally, we’ll add an app.py
file in the root level, to run our Flask app. Your folder should look like this:
day_two_final
├── app.py
├── repos
│ ├── exceptions.py
│ ├── models.py
│ └── api.py
├── static
│ ├── favicon.png
│ └── style.css
└── templates
├── error.html
└── index.html
exceptions.py
Let’s start with building a custom exception to handle API errors. Remember that is response.status_code
is anything but 200
, you can consider that an error. Create a GitHubApiException
class that subclasses Exception
. Have it accept a status_code
argument, and use that to create a custom message (you can copy the error strings we used yesterday). Pass the message to Exception
models.py
Next, let’s build our “model”, the GitHubRepo
class. For this, we want to accept three arguments (name
, language
, and num_stars
) and store them as instance variables (using self
). To have a user-friend way to print our repo information, add a __str__()
method that prints a message with the three repo parameters. For completeness, see if you can add a __repr__()
method that returns the Python code needed to recreate this object.
api.py
In our api.py
file, we’re going to copy in our create_query()
function and the repos_with_most_stars()
function from yesterday.
In create_query()
, see if you can clean up the code a little by replacing the for
loop with a string join that accepts a list comprehension.
Clean up your repos_with_most_stars()
function by using raise
to throw your GitHubAPIException
if the status_code
does not equal 200
. Then, instead of returning items directly from the response json, see if you can use a list comprehension to create and return a list of GitHubRepo
objects.
Don’t forget to import your GitHubApiException
, your GitHubRepo
class, and the requests
module.
app.py
Finally, let’s tie it all together with our app.py
file. We’ll start off with some boilerplate - we’ll need to import a few things from flask
, as well as our GitHubApiException
and our repos_with_most_stars()
function:
from flask import Flask, render_template, request
from repos.exceptions import GitHubApiException
from repos.api import repos_with_most_stars
Next, we’ll create the flask app
object. We’ll also create a list of all the available languages that the user of our web app can choose from. It will help us keep track of if they’re selected or not.
app = Flask(__name__)
available_languages = ["Python", "JavaScript", "Ruby", "Java"]
Next, we’ll need a function that gets called when the root url for our website, or /
is requested by the user
. We’ll start with the @app.route()
decorator - we didn’t cover decorators in this class, but just know that this signals to Flask that this index()
function should be called to handle any GET
or POST
requests to the URL /
.
@app.route('/', methods=['POST', 'GET'])
def index():
if request.method == 'GET':
# code for a GET
pass
elif request.method == 'POST':
# code for a POST
pass
We need to figure out which languages we have selected to determine which repos to display.
We’ll check the request.method
variable to determine what kind of request it was - if it was a GET
request, we’ll just display whichever repos were selected last (or all of them if this is the first request).
If it’s a POST
, we’ll grab the languages
variable from the request form and use it to populate our selected_languages
list:
if request.method == 'GET':
# Use the list of all languages
selected_languages = available_languages
elif request.method == 'POST':
# Use the languages we selected in the request form
selected_languages = request.form.getlist("languages")
Now, we just need to get our results and render our website. Call the repos_with_most_stars()
function in api.py
and pass it our selected_languages
.
Then, we’ll return our flask render_template()
function and pass it our list of selected languages, available languages, and our results.
results = repos_with_most_stars(selected_languages)
return render_template(
'index.html',
selected_languages=selected_languages,
available_languages=available_languages,
results=results)
Finally, we’ll add a custom error handler renders a special website (error.html
) if we receive a GitHubApiException
:
@app.errorhandler(GitHubApiException)
def handle_api_error(error):
return render_template('error.html', message=error)
Phew.
At last, make sure you’re in your root day_two_final
directory $ cd day_two_final
and start your webapp with debug mode.
(env) $ export FLASK_ENV=development; python3 -m flask run
* 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: 165-366-879
Point your web browser to the given URL (http://127.0.0.1:5000/ in this case) and you should see your list of repos sorted by number of stars. Play with the check boxes and the submit button on the left and see how the repo list changes. Congrats, you wrote a webapp in Python!
Let’s add some quick unit tests to our code, to make sure we don’t introduce any regressions later on. Create a file called test.py
in your day_two_final
folder. Your folder should look like this:
day_two_final
├── app.py
├── test.py
├── repos
│ ├── exceptions.py
│ ├── models.py
│ └── api.py
├── static
│ ├── favicon.png
│ └── style.css
└── templates
├── error.html
└── index.html
Create a new unittest.TestCase
called TestCreateQuery
. Inside, make a method called test_create_query()
. In this method, create a list of language names and an int
representing the a minimum number of stars. By looking at the create_query()
function, see if you can figure out what the correct query string should be. Call the create_query()
function with your test variables and use self.assertEqual()
to make sure they match. Don’t forget to import repos.api
and add your unittest.main()
invocation.
Run your test:
Let’s make two more quick tests of our GitHubApiException. Make a new TestCase
with two test functions. Name the class and functions using the same naming convention we’ve been using. For the first, we’ll use a fake status_code
of 403
. Create a GitHubApiException
object and pass it the status_code
. Check to see if the string “Rate limit” exists in the string representation of your exception (hint: use str()
).
For the second test, we’ll do the same thing with a status_code
of 500
. This time, we’ll check to see if “500” exists in the exception string.