http with python

This commit is contained in:
marcel-dempers 2021-10-05 20:26:34 +11:00
parent 9f23c5cfbf
commit 80773627a3
4 changed files with 338 additions and 115 deletions

View File

@ -3,10 +3,14 @@
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. </br>
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`.
@ -32,7 +36,9 @@ Python 3.9.6
## Our application
Firstly we have to import our dependencies
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
@ -79,153 +85,328 @@ def updateCustomers(customers):
customerFile.write(customerJSON)
```
Now to recap and test our functions we need to define a list of customers
to use:
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>
```
customers = {
"a": Customer("a","James", "Baker"),
"b": Customer("b", "Jonathan", "D"),
"c": Customer("c", "Aleem", "Janmohamed"),
"d": Customer("d", "Ivo", "Galic"),
"e": Customer("e", "Joel", "Griffiths"),
"f": Customer("f", "Michael", "Spinks"),
"g": Customer("g", "Victor", "Savkov"),
"h" : Customer("h", "Marcel", "Dempers")
{
"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 to convert our customer dictionary to JSON, we need to convert our dictionary of customer objects to a dictionary of customers where each customer is a dictionary instead of a class. </br>
To do this we generate a new dictionary and populate it by converting each customer object to a dictionary :
Now that we have our customer data, we can read and update the data with our two simple functions:
```
customerDict = {}
for id in customers:
customerDict[id] = customers[id].__dict__
```
Then we can finally test our update and get functionality:
```
updateCustomers(customerDict)
customers = getCustomers()
print(customers)
```
Let's test it and see the `customers.json` file stored to our server and
retrieved.
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
```
## External Libraries
## Flask
Checkout official Flask documentation [here](https://flask.palletsprojects.com/en/2.0.x/)
## JSON Library
Python provides a library for dealing with JSON. </br>
Let's take our customer dictionary, and convert it to json structure and
finally write it to file using our update function.
A minimal flask application looks like this:
```
import json
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
```
Now first things first, we need to convert our customers dictionary to a JSON structured string so we can store it to file. </br>
To get this to work, we need to look at Pythons Library system, or `pip`
The `json` library allows us to convert dictionaries to files using
the `dumps` function:
## 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:
```
jsonData = json.dumps(customers)
print(jsonData)
Flask == 2.0.1
```
But if we run this we get an error:
`TypeError: Object of type Customer is not JSON serializable`
That is because the library can only deal with full dictionaries.
Taking a closer look at our dictionary, our customer is a class.
We can install our dependencies using:
```
# our key is a string
# our value is a class
"h" : Customer("h", "Marcel", "Dempers")
pip install -r src/requirements.txt
```
`json.dumps()` only allows full dictionaries so we need our customer object to be in dictionary form like so:
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:
```
"h": { "customerID": "h", "firstName" : "Marcel", "lastName" : "Dempers"}
export FLASK_APP=src/app
flask run -h 0.0.0 -p 5000
```
To fix our dictionary, we can iterate it and build a new one
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.
```
# start with an empty one
customerDict = {}
exit
docker run -it -p 5000:5000 -v ${PWD}:/work python sh
#populate our new dictionary
for id in customers:
customerDict[id] = customers[id].__dict__
#print it
print(customerDict)
#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/`
Now we can convert our customer dictionary to json
## 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.
```
customerJSON = json.dumps(customerDict)
print(customerJSON)
* 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 -
```
Now we've converted our data into JSON, we can view this in a new tab window and change the language mode to JSON to visualise the data in our new format.
Now essentially our application will use dictionaries when working with the data
but store and transport it as JSON. </br>
Therefore our update function should take a dictionary and convert it to JSON
and store it.
Routes allow us to have many endpoints, so we could have endpoints such as:
```
def updateCustomers(customer):
with open('customers.json', 'w', newline='') as customerFile:
customerJSON = json.dumps(customer)
customerFile.write(customerJSON)
/ --> which returns all customers
/get/<customerID> --> which returns one customer by CustomerID
/add --> which adds or updates a customer
```
We also need to change our `getCustomers` function to read the JSON file
and convert that data correctly back to a dictionary for processing.
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:
```
def getCustomers():
if os.path.isfile("customers.json"):
with open('customers.json', newline='') as customerFile:
data = customerFile.read()
customers = json.loads(data)
return customers
@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 {}
return {}
```
Finally let's test our functions :
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.
```
customers = getCustomers()
print(customers)
@app.route("/get/<string:customerID>", methods=['GET'])
def get_customer(customerID):
customer = getCustomer(customerID)
if customer == {}:
return {}, 404
else:
return customer
```
It's also very easy to add a customer to our JSON data using our class and using pythons internal `__dict__` property to convert the object to a dictionary and add it to our customers dictionary
## 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>
Now we need to parse the request and validate it
```
customers["i"] = Customer("i", "Bob", "Smith").__dict__
updateCustomers(customers)
```
## Docker
@ -249,10 +430,31 @@ Build and run our container.
Notice the `customers.json` file gets created if it does not exist.
```
cd python\introduction\part-3.json
cd python\introduction\part-4.http
docker build . -t customer-app
docker run -v ${PWD}:/work -w /work customer-app
docker run -p 5000:5000 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
```

View File

@ -3,6 +3,11 @@ FROM python:3.9.6-alpine3.13 as dev
WORKDIR /work
FROM dev as runtime
COPY ./src/ /app
WORKDIR /app
COPY requirements.txt /app/
RUN pip install -r /app/requirements.txt
ENTRYPOINT [ "python", "/app/app.py" ]
COPY ./src/ /app
ENV FLASK_APP=app.py
CMD flask run -h 0.0.0 -p 5000

View File

@ -0,0 +1 @@
Flask == 2.0.1

View File

@ -1,7 +1,10 @@
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
@ -11,8 +14,8 @@ class Customer:
return self.firstName + " " + self.lastName
def getCustomers():
if os.path.isfile("customers.json"):
with open('customers.json', newline='') as customerFile:
if os.path.isfile(dataPath):
with open(dataPath, newline='') as customerFile:
data = customerFile.read()
customers = json.loads(data)
return customers
@ -20,32 +23,44 @@ def getCustomers():
return {}
def getCustomer(customerID):
customer = getCustomers()
return customer[customerID]
customers = getCustomers()
if customerID in customers:
return customers[customerID]
else:
return {}
def updateCustomers(customers):
with open('customers.json', 'w', newline='') as customerFile:
with open(dataPath, 'w', newline='') as customerFile:
customerJSON = json.dumps(customers)
customerFile.write(customerJSON)
app = Flask(__name__)
customers = {
"a": Customer("a","James", "Baker"),
"b": Customer("b", "Jonathan", "D"),
"c": Customer("c", "Aleem", "Janmohamed"),
"d": Customer("d", "Ivo", "Galic"),
"e": Customer("e", "Joel", "Griffiths"),
"f": Customer("f", "Michael", "Spinks"),
"g": Customer("g", "Victor", "Savkov"),
"h" : Customer("h", "Marcel", "Dempers")
}
@app.route("/", methods=['GET'])
def get_customers():
customers = getCustomers()
return json.dumps(customers)
customerDict = {}
@app.route("/get/<string:customerID>", methods=['GET'])
def get_customer(customerID):
customer = getCustomer(customerID)
if customer == {}:
return {}, 404
else:
return customer
for id in customers:
customerDict[id] = customers[id].__dict__
@app.route("/add", methods=['POST'])
def add_customer():
jsonData = request.json
updateCustomers(customerDict)
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()
print(customers)
customers = getCustomers()
customers[jsonData["customerID"]] = Customer( jsonData["customerID"], jsonData["firstName"], jsonData["lastName"]).__dict__
updateCustomers(customers)
return "success", 200