RESTful API with Python and Docker
Exercise: RESTful API with Python and Docker
Alert
These are my notes for an exercise class of my course Laboratory of cloud computing, big data and security, Università Cattolica del Sacro Cuore, ed. 2020. It can be difficult to follow this document without attending the lecture.
Preliminaries
We are going to install Docker on our VirtualBox Linux Mint VM. That's not mandatory, but it helps because with it we are all in the same environment.
Make sure to have python installed, or install it with:
$ sudo apt install python3
Goals
- We will develop a simple HTTP RESTful API with Python and we will dockerize it.
- Our application will BE a REST API and at the same time will USE other REST APIs.
- Our API will compute the daylight time for a given city:
- input: the name of the city
- output: the sunrise time, the sunset time, and the daylight time
Weather API
First, we have a look at OpenWeatherMap, a professional API for weather-related stuff.
https://openweathermap.org/
Take a look at the pricing model: https://openweathermap.org/price
We will use the free
The API key...
Set the environment variable for the key:
$ export APIKEY=7c5...
Test the API from the console:
$ curl "http://api.openweathermap.org/data/2.5/weather?q=brescia&appid=$APIKEY" {"coord":{"lon":10.3,"lat":45.63},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"base":"stations","main":{"temp":288.71,"feels_like":287.3,"temp_min":287.59,"temp_max":289.26,"pressure":1018,"humidity":64},"visibility":10000,"wind":{"speed":1.62,"deg":249},"clouds":{"all":100},"dt":1605619763,"sys":{"type":3,"id":197942,"country":"IT","sunrise":1605594088,"sunset":1605627988},"timezone":3600,"id":3181553,"name":"Provincia di Brescia","cod":200}
Note: for security reason, it's better never store keys in "public" code or text files (Dockerfile included).
Use the API with python
File prova_api.py
.
import requests import os APIKEY = os.environ['APIKEY'] # reads the environment variable response = requests.get("http://api.openweathermap.org/data/2.5/weather?q=brescia&appid="+APIKEY) print(response.status_code) print(response.json())
$ python3 prova_api.py 200 {'coord': {'lon': 10.3, 'lat': 45.63}, 'weather': [{'id': 804, 'main': 'Clouds', 'description': 'overcast clouds', 'icon': '04d'}], 'base': 'stations', 'main': {'temp': 288.71, 'feels_like': 287.3, 'temp_min': 287.59, 'temp_max': 289.26, 'pressure': 1018, 'humidity': 64}, 'visibility': 10000, 'wind': {'speed': 1.62, 'deg': 249}, 'clouds': {'all': 100}, 'dt': 1605619763, 'sys': {'type': 3, 'id': 197942, 'country': 'IT', 'sunrise': 1605594088, 'sunset': 1605627988}, 'timezone': 3600, 'id': 3181553, 'name': 'Provincia di Brescia', 'cod': 200}
My API with Flask
First, install flask. Flask is a simple web server written in Python.
$ sudo apt install python3-flask
Let's write our preliminary app in the file myapi.py
.
import flask app = flask.Flask(__name__) app.config["DEBUG"] = True @app.route('/', methods=['GET']) def home(): html = """ <h1>Sunrise-sunset API</h1> <p>Example RESTful API for the course Lab. of Cloud Computing, Big Data and security @ UniCatt</p> """ return html app.run()
And run it with the command:
$ python3 myapi.py
Try it by connecting to http://localhost:5000/
from the web browser.
Let's add a new entry-point named /api/prova
and return a JSON for testing.
import flask app = flask.Flask(__name__) app.config["DEBUG"] = True @app.route('/', methods=['GET']) def home(): html = """ <h1>Sunrise-sunset API</h1> <p>Example RESTful API for the course Lab. of Cloud Computing, Big Data and security @ UniCatt</p> """ return html @app.route('/api/prova', methods=['GET']) def api_prova(): output = { 'sunrise': 1, 'sunset': 2, 'city': 'Brescia' } return flask.jsonify(output) app.run()
Reading parameters
import flask from flask import request, jsonify app = flask.Flask(__name__) app.config["DEBUG"] = True @app.route('/', methods=['GET']) def home(): html = """ <h1>Sunrise-sunset API</h1> <p>Example RESTful API for the course Lab. of Cloud Computing, Big Data and security @ UniCatt</p> """ return html @app.route('/api/prova', methods=['GET']) def api_prova(): city = request.args.get('city') output = {} if city: output = { 'sunrise': 1, 'sunset': 2, 'city': city } return jsonify(output) app.run()
Alternative, arguably more elegant, way:
@app.route('/api/prova/<city>', methods=['GET']) def api_prova(city): output = {} if city: output = { 'sunrise': 1, 'sunset': 2, 'city': city } return jsonify(output)
Retrieve data from REST API, elaborate it and return it
Now, we retrieve the weather data from the OpenWeatherMap API, we elaborate and return the result.
The goal is to add a new entrypoint in our API, namely /api/daylight/<city>
, and return the daylight, i.e., the timespan between the sunrise and the sunset.
@app.route('/api/daylight/<city>', methods=['GET']) def api_daylight(city): output = {} uri = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={APIKEY}" print(uri) res = requests.get(uri) if res.status_code == 200: data = res.json() sunrise = data['sys']['sunrise'] sunset = data['sys']['sunset'] output = { 'sunrise': sunrise, 'sunset': sunset, 'daylight': sunset-sunrise, 'city': city } return output
Exercise: make the result more human-readable, such as
{ "city": "Brescia", "daylight": "9:25:00", "sunrise": "Tue, 17 Nov 2020 07:21:28 GMT", "sunset": "Tue, 17 Nov 2020 16:46:28 GMT" }
TIP: use the datetime
module.
Check the solution
# [at the import section]
from datetime import datetime, timedelta
# [all the other code]
@app.route('/api/daylight/<city>', methods=['GET'])
def api_daylight(city):
output = {}
uri = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={APIKEY}"
print(uri)
res = requests.get(uri)
if res.status_code == 200:
data = res.json()
sunrise = datetime.fromtimestamp(data['sys']['sunrise'])
sunset = datetime.fromtimestamp(data['sys']['sunset'])
daylight = sunset-sunrise
output = {
'sunrise': sunrise,
'sunset': sunset,
'daylight': str(daylight),
'city': city
}
return output
BUT THINK: is it a good idea to make things more human-readable at the price of less machine-readability in a REST API?
Exercise: add another entrypoint for the API with the city as input and the current temperature and the temperature range as output
Example:
$ curl "http://localhost:5000/api/temperature/brescia" { "city": "brescia", "temp": 14, "temp_min": 13, "temp_max": 16, "temp_range": 3 }
Dockerization
Since Flask accepts connection only from localhost by default and we are going to connect through a Docker virtual network, we need to explicitly allow connections from everywhere.
So, we need to modify the last line of the myapi.py
file to:
app.run(host='0.0.0.0') # accept connection from every host
File requirements.txt
:
flask==1.1.1 requests==2.22.0
File Dockerfile
:
FROM python:3.8-buster WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY myapi.py . EXPOSE 5000 CMD ["python", "myapi.py"]
Build the image with:
$ docker build --tag myapi .
Run the container with:
$ docker run -p 5000:5000 --env APIKEY=$APIKEY --rm myapi * Serving Flask app "myapi" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 610-128-137 172.17.0.1 - - [16/Nov/2020 16:42:28] "GET /api/daylight/Brescia HTTP/1.1" 200 -
Note that this is configuration is not recommended in production. In production we need to set up a WSGI server (e.g. gunicorn) and a reverse proxy (e.g. nginx). See for example https://towardsdatascience.com/how-to-deploy-ml-models-using-flask-gunicorn-nginx-docker-9b32055b3d0.
Exercise
We now want to develop a REST API that given a train id number (e.g. S11119/8810
) will output:
- where the train will meet the sunrise and/or the sunset
- how long the journey is during the daylight time and how long during the night
Example:
$ curl "http://localhost:5000/api/trainlight/S11119/8810" { "way": { "from": "BARI CENTRALE", "start_time": "Tue, 17 Nov 2020 05:30:00", "to": "MILANO CENTRALE", "destination_time": Tue, 17 Nov 2020 12:55:00" }, "sunrise": { "time": "Tue, 17 Nov 2020 06:54:00", "location": ["FOGGIA", "TERMOLI"] }, "daytime": "06:01:00", "nighttime": "01:24:00" }
NOTE: the entire exercise is challenging. Try to divide it into sub-problems and address one sub-problem at a time.
API viaggiatreno
To get information about trains in Italy, take a look at:
- https://github.com/bluviolin/TrainMonitor/wiki/API-del-sistema-Viaggiatreno
- http://www.viaggiatreno.it/viaggiatrenonew/resteasy/viaggiatreno/andamentoTreno/S11119/8810
Compare viaggiatreno API with OpenWeatherMap API. Which one is better? Why?