Federico Zacayan

Software Devloper.

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!