mirror of
https://github.com/marcel-dempers/docker-development-youtube-series.git
synced 2025-06-06 17:01:30 +00:00
commit
d52fb4d739
459
python/introduction/part-4.http/README.md
Normal file
459
python/introduction/part-4.http/README.md
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
# Introduction to Python: HTTP (Flask)
|
||||||
|
|
||||||
|
So far, we've learnt Python fundamentals, worked with data, files and
|
||||||
|
more importantly, basic data structures like `csv` and `json`.
|
||||||
|
|
||||||
|
JSON is a very popular format for now only storing, but transporting data in web HTTP applications. Files are important because we may
|
||||||
|
need to read configuration files for our application. </br>
|
||||||
|
|
||||||
|
One application could make a web request to ask for customer data from our application. By sending back JSON, the receiving application can easily process the data it gets.
|
||||||
|
|
||||||
|
Be sure to checkout [Part 2: Files](../part-2.files) </br>
|
||||||
|
As well as [Part 3: JSON](../part-3.json) </br>
|
||||||
|
|
||||||
|
## Python Dev Environment
|
||||||
|
|
||||||
|
The same as Part 1, we start with a [dockerfile](./dockerfile) where we declare our version of `python`.
|
||||||
|
|
||||||
|
```
|
||||||
|
FROM python:3.9.6-alpine3.13 as dev
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's build and start our container:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd python\introduction\part-4.http
|
||||||
|
|
||||||
|
docker build --target dev . -t python
|
||||||
|
docker run -it -v ${PWD}:/work python sh
|
||||||
|
|
||||||
|
/work # python --version
|
||||||
|
Python 3.9.6
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Our application
|
||||||
|
|
||||||
|
We're going to use what we've learnt in part 1,2 & 3 and create
|
||||||
|
our customer app that stores customer data </br>
|
||||||
|
Firstly we have to import our dependencies:
|
||||||
|
|
||||||
|
```
|
||||||
|
import os.path
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we have a class to define what a customer looks like:
|
||||||
|
```
|
||||||
|
class Customer:
|
||||||
|
def __init__(self, c="",f="",l=""):
|
||||||
|
self.customerID = c
|
||||||
|
self.firstName = f
|
||||||
|
self.lastName = l
|
||||||
|
def fullName(self):
|
||||||
|
return self.firstName + " " + self.lastName
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we need a function which returns our customers:
|
||||||
|
```
|
||||||
|
def getCustomers():
|
||||||
|
if os.path.isfile("customers.json"):
|
||||||
|
with open('customers.json', newline='') as customerFile:
|
||||||
|
data = customerFile.read()
|
||||||
|
customers = json.loads(data)
|
||||||
|
return customers
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is a function to return a specific customer:
|
||||||
|
```
|
||||||
|
def getCustomer(customerID):
|
||||||
|
customer = getCustomers()
|
||||||
|
return customer[customerID]
|
||||||
|
```
|
||||||
|
And finally a function for updating our customers:
|
||||||
|
|
||||||
|
```
|
||||||
|
def updateCustomers(customers):
|
||||||
|
with open('customers.json', 'w', newline='') as customerFile:
|
||||||
|
customerJSON = json.dumps(customers)
|
||||||
|
customerFile.write(customerJSON)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the previous episode, we've created a `json` file to hold all our customers. </br>
|
||||||
|
We've learnt how to read and write to file and temporarily use the file for our storage. </br>
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"a": {
|
||||||
|
"customerID": "a",
|
||||||
|
"firstName": "James",
|
||||||
|
"lastName": "Baker"
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"customerID": "b",
|
||||||
|
"firstName": "Jonathan",
|
||||||
|
"lastName": "D"
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
"customerID": "c",
|
||||||
|
"firstName": "Aleem",
|
||||||
|
"lastName": "Janmohamed"
|
||||||
|
},
|
||||||
|
"d": {
|
||||||
|
"customerID": "d",
|
||||||
|
"firstName": "Ivo",
|
||||||
|
"lastName": "Galic"
|
||||||
|
},
|
||||||
|
"e": {
|
||||||
|
"customerID": "e",
|
||||||
|
"firstName": "Joel",
|
||||||
|
"lastName": "Griffiths"
|
||||||
|
},
|
||||||
|
"f": {
|
||||||
|
"customerID": "f",
|
||||||
|
"firstName": "Michael",
|
||||||
|
"lastName": "Spinks"
|
||||||
|
},
|
||||||
|
"g": {
|
||||||
|
"customerID": "g",
|
||||||
|
"firstName": "Victor",
|
||||||
|
"lastName": "Savkov"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we have our customer data, we can read and update the data with our two simple functions:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
customers = getCustomers()
|
||||||
|
print(customers)
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's add a customer and write it back to file:
|
||||||
|
|
||||||
|
```
|
||||||
|
customers["h"] = Customer("h", "Marcel", "Dempers").__dict__
|
||||||
|
updateCustomers(customers)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now `customers.json` has the new entry if we rerun our code:
|
||||||
|
|
||||||
|
```
|
||||||
|
python src/app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flask
|
||||||
|
|
||||||
|
Checkout official Flask documentation [here](https://flask.palletsprojects.com/en/2.0.x/)
|
||||||
|
|
||||||
|
A minimal flask application looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def hello_world():
|
||||||
|
return "<p>Hello, World!</p>"
|
||||||
|
```
|
||||||
|
|
||||||
|
To get this to work, we need to look at Pythons Library system, or `pip`
|
||||||
|
|
||||||
|
## External Packages
|
||||||
|
|
||||||
|
If we look at the `from flask import Flask` statement, it assumes we have a `flask` dependency. Our previous import statements worked, because they are built-in libraries in python. </br>
|
||||||
|
Flask is an external package. </br>
|
||||||
|
External packages are managed by [pip](https://pypi.org/project/pip/)
|
||||||
|
|
||||||
|
We may use `pip` to install `Flask`. If we need to connect to databases, we may install packages to do so. Like a `mysql` or a `redis` package. </br>
|
||||||
|
|
||||||
|
With `pip`, we define our dependencies in a `requirements.txt` file.
|
||||||
|
We can check for the latest version of `Flask` [here](https://pypi.org/project/Flask/)
|
||||||
|
|
||||||
|
We can also use `pip install` commands to install dependencies. </br>
|
||||||
|
However, this means we need a new `pip install` command for every dependency which will grow as
|
||||||
|
our application and needs grow. Best is to use a `requirements.txt` file.
|
||||||
|
|
||||||
|
Let's create a `requirements.txt` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
Flask == 2.0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
We can install our dependencies using:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install -r src/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's implement the minimal example we posted above. </br>
|
||||||
|
To run our application, note that the `app.py` runs as per usual. <br/>
|
||||||
|
But to start `Flask`, we can follow the instructions as per the official document:
|
||||||
|
|
||||||
|
```
|
||||||
|
export FLASK_APP=src/app
|
||||||
|
flask run -h 0.0.0 -p 5000
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that we cannot access `http://localhost:5000` because our app is running in docker. </br>
|
||||||
|
To access it, we need to restart our container and this time expose our port.
|
||||||
|
|
||||||
|
```
|
||||||
|
exit
|
||||||
|
docker run -it -p 5000:5000 -v ${PWD}:/work python sh
|
||||||
|
|
||||||
|
#get our dependencies and start our application
|
||||||
|
pip install -r requirements.txt
|
||||||
|
export FLASK_APP=src/app
|
||||||
|
flask run -h 0.0.0 -p 5000
|
||||||
|
```
|
||||||
|
Now we can access our app on `http://localhost:5000/`
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
Web servers allow us to define various URLs or routes. </br>
|
||||||
|
Currently we can see logs showing `GET` requests for route `/` </br>
|
||||||
|
Notice the HTTP status code `200` indicating success.
|
||||||
|
|
||||||
|
```
|
||||||
|
* Running on http://0.0.0:5000/ (Press CTRL+C to quit)
|
||||||
|
172.17.0.1 - - [22/Sep/2021 23:10:14] "GET / HTTP/1.1" 200 -
|
||||||
|
172.17.0.1 - - [22/Sep/2021 23:10:14] "GET /favicon.ico HTTP/1.1" 404 -
|
||||||
|
172.17.0.1 - - [22/Sep/2021 23:12:20] "GET / HTTP/1.1" 200 -
|
||||||
|
172.17.0.1 - - [22/Sep/2021 23:12:21] "GET / HTTP/1.1" 200 -
|
||||||
|
172.17.0.1 - - [22/Sep/2021 23:12:21] "GET / HTTP/1.1" 200 -
|
||||||
|
```
|
||||||
|
|
||||||
|
Routes allow us to have many endpoints, so we could have endpoints such as:
|
||||||
|
|
||||||
|
```
|
||||||
|
/all --> which returns all customers
|
||||||
|
/get/<customerID> --> which returns one customer by CustomerID
|
||||||
|
/add --> which adds or updates a customer
|
||||||
|
```
|
||||||
|
|
||||||
|
Defining these routes are pretty straight forward, see [docs](https://flask.palletsprojects.com/en/2.0.x/quickstart/#variable-rules)
|
||||||
|
|
||||||
|
Let's define our routes:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def get_customers():
|
||||||
|
return "<p>Hello, get_customers!</p>"
|
||||||
|
|
||||||
|
@app.route("/get/<string:customerID>", methods=['GET'])
|
||||||
|
def get_customer(customerID):
|
||||||
|
return "<p>Hello, get!</p>"
|
||||||
|
|
||||||
|
@app.route("/add", methods=['POST'])
|
||||||
|
def add_customer(customer):
|
||||||
|
return "<p>Hello, add!</p>"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've built our URLs using routes, next we need to understand HTTP methods
|
||||||
|
|
||||||
|
## HTTP Methods
|
||||||
|
|
||||||
|
There are a number of HTTP methods for web services, popular ones being `POST` and `GET`
|
||||||
|
|
||||||
|
`GET` method is for general retrieval of data </br>
|
||||||
|
`POST` method is for passing data to our service </br>
|
||||||
|
|
||||||
|
Let's setup each of our routes with dedicated HTTP Methods
|
||||||
|
|
||||||
|
```
|
||||||
|
@app.route("/", methods=['GET'])
|
||||||
|
def get_customers():
|
||||||
|
return "<p>Hello, get_customers!</p>"
|
||||||
|
|
||||||
|
@app.route("/get/<string:customerID>", methods=['GET'])
|
||||||
|
def get_customer(customerID):
|
||||||
|
return "<p>Hello, get!</p>"
|
||||||
|
|
||||||
|
@app.route("/add", methods=['POST'])
|
||||||
|
def add_customer():
|
||||||
|
return "<p>Hello, add!</p>"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can see if we access `http://localhost:5000/add`
|
||||||
|
We get: `Method Not Allowed` , because it only accepts `POST` and browser by default, does `GET`
|
||||||
|
|
||||||
|
Let's fill out our routes
|
||||||
|
|
||||||
|
### Get Customers
|
||||||
|
|
||||||
|
```
|
||||||
|
@app.route("/", methods=['GET'])
|
||||||
|
def get_customers():
|
||||||
|
customers = getCustomers()
|
||||||
|
return json.dumps(customers)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Customer
|
||||||
|
|
||||||
|
```
|
||||||
|
@app.route("/get/<string:customerID>", methods=['GET'])
|
||||||
|
def get_customer(customerID):
|
||||||
|
customer = getCustomer(customerID)
|
||||||
|
return customer
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Status Codes
|
||||||
|
|
||||||
|
You can access your browser's developer tools and see the network tab </br>
|
||||||
|
Status codes are important for troubleshooting production traffic.
|
||||||
|
|
||||||
|
`200` = Success. </br>
|
||||||
|
|
||||||
|
We can then access a customer via the `/get/<customerID>` route.
|
||||||
|
Our Customer ID's are `a` to `h` and notice we get an `Internal Server Error` if
|
||||||
|
we pass an incorrect ID. </br>
|
||||||
|
|
||||||
|
`500` = Internal Server Error </br>
|
||||||
|
This means something went wrong in our code. See the logs indicating a `KeyError`, because we're trying to access a key in our customer dictionary that does not exist. </br>
|
||||||
|
|
||||||
|
Let's handle that error
|
||||||
|
|
||||||
|
```
|
||||||
|
#update our getCustomer function to check if the key exists
|
||||||
|
|
||||||
|
def getCustomer(customerID):
|
||||||
|
customers = getCustomers()
|
||||||
|
if customerID in customers:
|
||||||
|
return customers[customerID]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note when we rerun this, we get a blank customer back. </br>
|
||||||
|
Also note we're getting a `200` status code. </br>
|
||||||
|
This may not be super appropriate, as our customer we requested is technically not found. There is a status code for that </br>
|
||||||
|
|
||||||
|
`404` = Not Found </br>
|
||||||
|
|
||||||
|
Let's handle the status code.
|
||||||
|
|
||||||
|
```
|
||||||
|
@app.route("/get/<string:customerID>", methods=['GET'])
|
||||||
|
def get_customer(customerID):
|
||||||
|
customer = getCustomer(customerID)
|
||||||
|
if customer == {}:
|
||||||
|
return {}, 404
|
||||||
|
else:
|
||||||
|
return customer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add Customer
|
||||||
|
|
||||||
|
In order to add a customer, we need to be able to read request data. </br>
|
||||||
|
More about `Requests` [here](https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request)
|
||||||
|
|
||||||
|
Import requests library
|
||||||
|
```
|
||||||
|
from flask import request
|
||||||
|
```
|
||||||
|
|
||||||
|
Read the request body as JSON:
|
||||||
|
|
||||||
|
```
|
||||||
|
@app.route("/add", methods=['POST'])
|
||||||
|
def add_customer():
|
||||||
|
print(request.json)
|
||||||
|
return "success", 200
|
||||||
|
```
|
||||||
|
Let's test that!
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST http://localhost:5000/add -d '{ "data" : "hello" }'
|
||||||
|
```
|
||||||
|
Notice our print message is `None`, this is because we need to specify content type in the request header. Let's send the content type as JSON.
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
http://localhost:5000/add -d '{ "data" : "hello" }'
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we see that we read the data as `{'data': 'hello'}` </br>
|
||||||
|
This allows us to read customer data as JSON, validate it, and write it to storage. </br>
|
||||||
|
|
||||||
|
Let's `POST` a customer:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST -v \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
http://localhost:5000/add -d '
|
||||||
|
{
|
||||||
|
"customerID": "i",
|
||||||
|
"firstName": "Bob",
|
||||||
|
"lastName": "Smith"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
Note if we break the `json` format, we get a `400` status code </br>
|
||||||
|
|
||||||
|
`400` = Bad Request </br>
|
||||||
|
|
||||||
|
400 indicates a bad request, letting the client know they have to send the right formatted data.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
Let's build our container image and run it while mounting our customer file
|
||||||
|
|
||||||
|
Our final `dockerfile`
|
||||||
|
```
|
||||||
|
FROM python:3.9.6-alpine3.13 as dev
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
FROM dev as runtime
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ./src/requirements.txt /app/
|
||||||
|
RUN pip install -r /app/requirements.txt
|
||||||
|
|
||||||
|
COPY ./src/app.py /app/app.py
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
|
||||||
|
CMD flask run -h 0.0.0 -p 5000
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Build our container.
|
||||||
|
|
||||||
|
```
|
||||||
|
cd python\introduction\part-4.http
|
||||||
|
|
||||||
|
docker build . -t customer-app
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that we need to mount our `customers.json` file into the container. </br>
|
||||||
|
But also notice the containers working directory is `/app` and in the `app.py`
|
||||||
|
we load `customers.json` from the working directory which is the same directory as the app. </br>
|
||||||
|
When mounting files to a container, docker effectively formats that path, so to mount our json file, we would lose our `app.py` because of the format. <br/>
|
||||||
|
|
||||||
|
Therefore it's always important to mount configs, secrets or data to seperate folders.
|
||||||
|
|
||||||
|
Let's change that to `/data`
|
||||||
|
|
||||||
|
```
|
||||||
|
#set a global variable
|
||||||
|
dataPath = "/data/customers.json"
|
||||||
|
```
|
||||||
|
Now we can change all references to `customers.json` to our variable.
|
||||||
|
And then mount that location:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build . -t customer-app
|
||||||
|
docker run -it -p 5000:5000 -v ${PWD}:/data customer-app
|
||||||
|
```
|
14
python/introduction/part-4.http/dockerfile
Normal file
14
python/introduction/part-4.http/dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.9.6-alpine3.13 as dev
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
FROM dev as runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./src/requirements.txt /app/
|
||||||
|
RUN pip install -r /app/requirements.txt
|
||||||
|
|
||||||
|
COPY ./src/app.py /app/app.py
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
|
||||||
|
CMD flask run -h 0.0.0 -p 5000
|
68
python/introduction/part-4.http/src/app.py
Normal file
68
python/introduction/part-4.http/src/app.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import os.path
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from flask import Flask
|
||||||
|
from flask import request
|
||||||
|
|
||||||
|
dataPath = "/data/customers.json"
|
||||||
|
class Customer:
|
||||||
|
def __init__(self, c="",f="",l=""):
|
||||||
|
self.customerID = c
|
||||||
|
self.firstName = f
|
||||||
|
self.lastName = l
|
||||||
|
def fullName(self):
|
||||||
|
return self.firstName + " " + self.lastName
|
||||||
|
|
||||||
|
def getCustomers():
|
||||||
|
if os.path.isfile(dataPath):
|
||||||
|
with open(dataPath, newline='') as customerFile:
|
||||||
|
data = customerFile.read()
|
||||||
|
customers = json.loads(data)
|
||||||
|
return customers
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def getCustomer(customerID):
|
||||||
|
customers = getCustomers()
|
||||||
|
|
||||||
|
if customerID in customers:
|
||||||
|
return customers[customerID]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def updateCustomers(customers):
|
||||||
|
with open(dataPath, 'w', newline='') as customerFile:
|
||||||
|
customerJSON = json.dumps(customers)
|
||||||
|
customerFile.write(customerJSON)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route("/", methods=['GET'])
|
||||||
|
def get_customers():
|
||||||
|
customers = getCustomers()
|
||||||
|
return json.dumps(customers)
|
||||||
|
|
||||||
|
@app.route("/get/<string:customerID>", methods=['GET'])
|
||||||
|
def get_customer(customerID):
|
||||||
|
customer = getCustomer(customerID)
|
||||||
|
|
||||||
|
if customer == {}:
|
||||||
|
return {}, 404
|
||||||
|
else:
|
||||||
|
return customer
|
||||||
|
|
||||||
|
@app.route("/add", methods=['POST'])
|
||||||
|
def add_customer():
|
||||||
|
jsonData = request.json
|
||||||
|
|
||||||
|
if "customerID" not in jsonData:
|
||||||
|
return "customerID required", 400
|
||||||
|
if "firstName" not in jsonData:
|
||||||
|
return "firstName required", 400
|
||||||
|
if "lastName" not in jsonData:
|
||||||
|
return "lastName required", 400
|
||||||
|
|
||||||
|
customers = getCustomers()
|
||||||
|
customers[jsonData["customerID"]] = Customer( jsonData["customerID"], jsonData["firstName"], jsonData["lastName"]).__dict__
|
||||||
|
updateCustomers(customers)
|
||||||
|
return "success", 200
|
1
python/introduction/part-4.http/src/requirements.txt
Normal file
1
python/introduction/part-4.http/src/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Flask == 2.0.2
|
Loading…
x
Reference in New Issue
Block a user