Python
Unfortunately, the dependencies for any python project are installed globally in the Operative System.
But, thanks to freeze command, we can create easily the file requirements.txt that we will need to implement this project using Docker.
pip freeze > requirements.txt
The result of this command is a file named requirements.txt.
Click==7.0
Flask==1.1.1
flask-marshmallow==0.10.1
Flask-SQLAlchemy==2.4.0
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.1
marshmallow==3.2.0
marshmallow-sqlalchemy==0.19.0
six==1.12.0
SQLAlchemy==1.3.8
Werkzeug==0.16.0
So, let us go straight to create a folders needed for our python project in /python-project/docpyenv.
mkdir python-project && mkdir python-project/docpyenv && cd python-project/docpyenv
In the folder /python-project/docpyenv create Dockerfile file, following the demo officialy offered in https://hub.docker.com/_/python.
FROM python:3
#WORKDIR /usr/src/app
COPY requirements.txt ./
#RUN pip install --no-cache-dir -r requirements.txt
RUN pip install -r requirements.txt
#COPY . .
#CMD [ "python", "./your-daemon-or-script.py" ]
########################################################
# sudo docker build -t pyenv:1.0 . #
########################################################
########################################################
# sudo docker rmi pyenv:1.0 #
########################################################
We have just commented some unnecessary lines and have added two commands to build and destroy the base image which we will use as environment.
In this way, we have documented how to proceed in case we need to build this image.
As you can see, we tagged this image as pyenv:1.0.
To build this image, just place requirements.txt in the /python-project/docpyenv folder, in the same level of Dockerfile, and run this command on that location.
sudo docker build -t pyenv:1.0 .
It takes a while. After pull the image python:3 and download all the dependencies on it, you can see the image created.
sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
pyenv 1.0 056847527e06 18 seconds ago 937MB
We can destroy this image with the following command.
sudo docker rmi pyenv:1.0
But, we do not have to do it now. The images have to remain for a while.
Now, we are able to develop our local "Hello world" API RESTful web server application on Python!
In the folder /python-tutorial create app.py file.
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/', methods=['GET'])
def get_products():
return jsonify({'msg':'Hello world'}), 200
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
In the same folder create a new Dockerfile.
FROM pyenv:1.0
WORKDIR /usr/src/app
COPY . .
ENTRYPOINT ["python"]
CMD ["app.py"]
############### TO BUILD ###################
# sudo docker build -t pyenv:2.0 .
# sudo docker run -p 5000:5000 --name deploywith pyenv:2.0
############### TO CLEAN ###################
# sudo docker rm deploywith
# sudo docker rmi pyenv:2.0
To run the web server execute the following commands in the same /python-tutorial folder.
sudo docker build -t pyenv:2.0 . && sudo docker run -p 5000:5000 --name deploywith pyenv:2.0
Now you can test this server application with a API REST consumer application. The most popular at the moment is Postman.
GET http://localhost:5000
The answer will be...
{
"msg": "Hello world"
}
If you make any change in your code you have to re-build the image pyenv:2.0.
To re-build after a modification in your code destroy the container (named deploywith) and his image (tagged pyenv:2.0)
sudo docker rm deploywith && sudo docker rmi pyenv:2.0
After that, you can run the server, as we saw few steps before, building the image and his container, just in one line.
sudo docker build -t pyenv:2.0 . && sudo docker run -p 5000:5000 --name deploywith pyenv:2.0
We are assuming you are developing, and that is why we did not mention you can run the server in detached mode.
To do that just add "-d" flag to the container command.
sudo docker build -t pyenv:2.0 . && sudo docker run -d -p 5000:5000 --name deploywith pyenv:2.0
In this way, the local web server can run in background and the terminal is released to allow you keep working.
If you run docker in detached mode you can stop its process with the following command in the /python-project.
sudo docker stop deploywith
And after, destroy the container and his image as you already know, in case you need it.
By the way, you can see the running and sleeping containers and his corresponded images. Just, look the columns IMAGE and STATUS.
sudo docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bd8399c28da2 pyenv:2.0 "python app.py" 19 minutes ago Exited (0) 18 minutes ago deploywith
There is a chance you can use with this project as part of a bigger project which include this one as a microservice.
If that is the case, you could need to run this web server as a service using docker-compose.
You, just would create a /python-tutorial/docker-compose.yml file.
version : '3.4'
services:
api:
build: .
volumes:
- ./usr/src/app
ports:
- 5000:5000
And you would use the following command to deploy your whole project including this REST API .
sudo docker-compose up # or sudo docker-compose up -d
And then, remove container and image with...
sudo docker rm python-project_api_1 && sudo docker rmi python-project_api
Waring!: the container and images names could change depending on Docker or docker-compose version.
You can find the source until now in github repository.
git clone https://github.com/federicozacayan/restful-api-python.git python-project
The you can list the commits.
git log --oneline
The you can go to the commit 'Hello World'.
git checkout 76eefc3
Now, it is moment to improve our Hello World application.
To begin from the beginning, we will create a package named flaskapi (lowercase).
This package will be called by our main file /python-project/app.py
from flaskapi import app
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
The packages are basically folders. But, their names will be the names of the packages what we will use in our source code.
Into the folder we have the special file name __init__.py which is the main file in the package..
Let us go to create the /python-project/flaskapi folder.
mkdir flaskapi
Now, the the /python-project/flaskapi/__init__.py file.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
from flaskapi import routes, errors
As you can see, at the end of the file we need to import and create routes and errors.
We can start by /python-tutorial/flaskapi/errors.py.
from flask import jsonify
from flaskapi import app
@app.errorhandler(404)
def not_found(e):
return jsonify({'error':'Not Data found'}), 404
@app.errorhandler(405)
def not_found(e):
return jsonify({'error':'Method Not Allowed'}), 405
@app.errorhandler(400)
def not_found(e):
return jsonify({'error':'Bad request'}), 400
And continuing creating the /python-project/flaskapi/routes.py file.
from flask import request, jsonify
from flaskapi.models import Product
from flaskapi.schemas import ProductSchema, product_schema, products_schema
from flaskapi import app, db
# Get a Product
@app.route('/product/<id>', methods=['GET'])
def get_product(id):
product = Product.query.get(id)
product_schema = ProductSchema()
return product_schema.jsonify(product), 200
# Save a Product
@app.route('/product', methods=['POST'])
def addProduct():
name = request.json['name']
admin = Product(name=name)
db.session.add(admin)
db.session.commit()
return jsonify({'status':201}), 201
# Get All Products
@app.route('/product', methods=['GET'])
def getProducts():
products = Product.query.all()
n = db.session.query(Product.name).count()
output = products_schema.dump(products);
return jsonify({
'q': n,
'product' : output
}), 200
# Delete Product
@app.route('/product/<id>', methods=['DELETE'])
def deleteProduct(id):
try:
product = Product.query.get(id)
db.session.delete(product)
db.session.commit()
return jsonify({'status':'200'}), 200
except:
return jsonify({'status':'500'}), 500
# Update Product
@app.route('/product/<id>', methods=['PUT'])
def updateProduct(id):
product = Product.query.get(id)
name = request.json['name']
product.name = name
db.session.commit()
return product_schema.jsonify(product), 200
0
The approach we have to handle databases is setting up the data structure through Models.
Have a look to the /python-project/flaskapi/models.py file.
from flaskapi import db
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
def __repr__(self):
return '<Product %r>' % self.name
In order to convert the database results in json we need to use schemas.
That is way we need the following /python-project/flaskapi/schemas.py file.
from flask_marshmallow import Marshmallow
from flaskapi.models import Product
from flaskapi import app, db
ma = Marshmallow(app)
class ProductSchema(ma.ModelSchema):
class Meta:
model = Product
product_schema = ProductSchema()
products_schema = ProductSchema(many=True)
#create database after define schemas
db.create_all()
All the magic happen, when every code line is placed in the properly place.
For instance, when every file is imported or just one element is imported, the whole file is loaded and executed.
There is no problem to over import one file several times, but you must to have a good eye to avoid circular loading.
On the other hand, we are creating the database after every Model is loaded and not before.
Otherwise, the database could be created with models missing or even worse, with no models.
Now, is time to test the application!