# 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.
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)
As well as [Part 3: JSON](../part-3.json)
## 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
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.
We've learnt how to read and write to file and temporarily use the file for our storage.
``` { "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 "

Hello, World!

" ``` 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.
Flask is an external package.
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.
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.
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.
To run our application, note that the `app.py` runs as per usual.
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.
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.
Currently we can see logs showing `GET` requests for route `/`
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/ --> 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 "

Hello, get_customers!

" @app.route("/get/", methods=['GET']) def get_customer(customerID): return "

Hello, get!

" @app.route("/add", methods=['POST']) def add_customer(customer): return "

Hello, add!

" ``` 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
`POST` method is for passing data to our service
Let's setup each of our routes with dedicated HTTP Methods ``` @app.route("/", methods=['GET']) def get_customers(): return "

Hello, get_customers!

" @app.route("/get/", methods=['GET']) def get_customer(customerID): return "

Hello, get!

" @app.route("/add", methods=['POST']) def add_customer(): return "

Hello, add!

" ``` 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/", 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
Status codes are important for troubleshooting production traffic. `200` = Success.
We can then access a customer via the `/get/` route. Our Customer ID's are `a` to `h` and notice we get an `Internal Server Error` if we pass an incorrect ID.
`500` = Internal Server Error
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.
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.
Also note we're getting a `200` status code.
This may not be super appropriate, as our customer we requested is technically not found. There is a status code for that
`404` = Not Found
Let's handle the status code. ``` @app.route("/get/", 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.
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'}`
This allows us to read customer data as JSON, validate it, and write it to storage.
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
`400` = Bad Request
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.
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.
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.
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 ```