In networking clients are computers or programs that send requests; servers are computers or programs that respond to those requests.
Any web browser is an example of a client. You have also written Python programs with requests
and/or selenium
that make requests, and thus are clients.
In this reading, we'll focus on the other side: servers. As a data scientist, working with data dashboards (with live stats, plots, etc) is one scenario where you may find yourself writing server code.
There are many Python packages to help write web servers. Django is the most popular. We'll use the second most popular, Flask, as it is much easier to learn.
Before we make a web server, we'll create a selenium browser because we'll need a client to test our server's ability to respond to requests. We'll also create a simple visit
function that shows us what is available at a given URL.
import time
import requests
from IPython.core.display import Image
from multiprocessing import Process
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import NoSuchElementException
options = Options()
options.headless = True
b = webdriver.Chrome(options=options)
b.set_window_size(600, 400)
def visit(url, source=False):
if source:
r = requests.get(url)
return r.text
else:
b.get(url)
b.save_screenshot("tmp.png")
return Image("tmp.png")
# test it on the course website
visit("https://tyler.caraza-harter.com/cs320/s21/schedule.html")
print(visit("https://tyler.caraza-harter.com/cs320/s21/schedule.html", source=True)[:500] + "\n...")
We be using Flask application objects (which we can name as we please) to create web servers:
from flask import Flask, request
my320app = Flask("example-server")
type(my320app)
To add pages to a Flask application, you write a function (called a handler) for each page; the function returns a string containing HTML corresponding to the page.
Flask uses routes to map URLs to handler functions. Putting @APPLICATION_VARIABLE.route("/SOME_PATH_HERE")
before a function to make it a handler for the given URL.
@my320app.route("/")
def home():
return "Home"
@my320app.route("/hello.html")
def handler1():
return '<html><body style="background-color:lightblue"><i>hello</i><body></html>'
@my320app.route("/world.html")
def handler2():
return '<html><body style="background-color:lightgreen"><h1>world</h1><body></html>'
#if __name__ == '__main__':
# app.run("0.0.0.0", "5000")
Generally, Flask applications are written as .py files. You could paste the above two cells into a .py file, uncomment those last two lines, then run the .py file to start a server running your application. Requests may keep coming in indefinitely, so the app.run
function would essentially run forever, dealing with requests as they come in (well, at least until you manually killed it with CONTROL
-C
, pkill
, or some other method).
For the purposes of having our Flask server and selenium client in the same notebook, we'll do something a bit weird: run the server in the background from a cell, then send requests from the following code. The following code starts the flask application in the background (don't worry about understanding it, as you'll probably use .py files for your own Flask applications, unless you yourself are writing a document like this to document Flask).
p = Process(target=my320app.run)
p.start()
time.sleep(1) # give it time to get going
The "Running ..." line above shows that we can visit the page at http://127.0.0.1:5000/. Note that 127.0.0.1 is a special "local" IP address, referring to the computer where the code is running. Let's send requests to see the strings returned by those three pages. You can see "GET ..." messages above corresponding to page visits. As this notebook was executed, those messages were added above as code below executed.
visit("http://127.0.0.1:5000/", source=True) # calls "home" function after @my320app.route("/")
visit("http://127.0.0.1:5000/hello.html", source=True) # calls handler1
visit("http://127.0.0.1:5000/world.html", source=True) # calls handler2
If we try a page that doesn't exist (that is, no route maps it to a handler), Flask will automatically return HTML (as a string) containing an error message.
visit("http://127.0.0.1:5000/missing.html", source=True)
When a web browser receives one of the above strings containing HTML, it uses it to show a formatted page:
visit("http://127.0.0.1:5000/hello.html")
visit("http://127.0.0.1:5000/world.html")
The ":5000" part of the above URLs means Flask is listening on port 5000. Only one process can listen on each port at a time. We could start other Flask servers on other ports, but we will get an "Address already in use" error if we try again on the same port:
try:
my320app.run("127.0.0.1", "5000")
except OSError as e:
print(e)
We will need to terminate the previous background Flask instance before starting anything else on port 5000.
p.terminate()
Sometimes, we'll want to send input to a Flask handler that it can use when constructing the output. One way is with a query string.
Consider this URL: http://127.0.0.1:5000/hello.html?name=tyler
. There are a few parts to this:
The resource is what we'll use with the Flask route. The query string gets loaded to a kind of dictionary called request.args
, where "name" is a key and "tyler" is a value.
app2 = Flask("example-server")
@app2.route("/hello.html")
def welcome():
print(request.args)
username = request.args["name"]
return f'<h1>Welcome {username}!</h1>'
p = Process(target=app2.run)
p.start()
time.sleep(1) # give it time to get going
visit("http://127.0.0.1:5000/hello.html?name=tyler", source=True)
visit("http://127.0.0.1:5000/hello.html?name=tyler")
p.terminate()
If there are multiple arguments in the query string, you can just use the &
symbol to separate the key/value pairs: ?key1=val1&key2=val2
. Below is an example of pages that do arithmatic. They also use multi-line strings via the triple quotes. The .format
method replaces the {}
slots in the string template.
app2 = Flask("example-server")
@app2.route("/add.html")
def add():
x = float(request.args["x"])
y = float(request.args["y"])
total = x + y
html = """
<html>
<body style="background-color:lightblue">
<h1>ADD</h1>
{} + {} = {}
<body>
</html>
"""
return html.format(x, y, total)
@app2.route("/subtract.html")
def sub():
x = float(request.args["x"])
y = float(request.args["y"])
total = x - y
html = """
<html>
<body style="background-color:lightblue">
<h1>SUB</h1>
{} - {} = {}
<body>
</html>
"""
return html.format(x, y, total)
p = Process(target=app2.run)
p.start()
time.sleep(1) # give it time to get going
visit("http://127.0.0.1:5000/add.html?x=300&y=20")
visit("http://127.0.0.1:5000/subtract.html?x=1000&y=1")
p.terminate()
# cleanup Selenium
b.close()