Build and Deploy a REST API Microservice with Python Flask and Docker

swarnimwalavalkar profile imageSwarnim Walavalkar ・8 min read

Objectives:

  • Build a simple but real-world useable REST API
  • Follow REST and Microservice Arch Best Practices
  • Deploy to a Docker Container

Technologies used:

  • Python
  • Flask
  • Flask Restful
  • Docker

Prerequisites:

  • A Foundational Understanding of Python
  • Acquaintance with the Flask Microframework
  • A Primary Understanding of Docker and Docker Container Management

Difficulty: ⚡⚡Intermediate

Setup:

Make sure you have pipenv installed.

pip install pipenv

Then, start by making a directory for the project and cd into it

mkdir FlaskBookApi/
cd FlaskBookApi/

Then install the flask and flask-restful packages using pipenv. This will do a couple of things:

pipenv install flask flask-restful

It creates a python virtual environment with your directory's name in a central location and installs the specified packages (in this case, flask and flask-restful). Along with that, In your working directory, it creates the Pipfile, which contains all of the projects dependencies with their appropriate version numbers along with the Python version used by the project. It also creates the Pipfile.lock to enable deterministic builds in production.

This isn't a pipenv tutorial, So I won't go into too much detail about that.

Write The API:

Alright, Now the fun part! Lets Build this thing!

Start by creating a Python file in the working directory.

touch api.py

Great! Now import and initialize flask and flask restful

from flask import Flask
from flask_restful import Resource, Api, reqparse, abort, marshal, fields

# Initialize Flask
app = Flask(__name__)
api = Api(app)

Be sure to import the required modules from flask-restful as shown above.

We'll be building a simple API that goes over the performing some basic CRUD operations on a datastore of books (or as it happens, in this case, a List of Dictionaries).

We're going to use a simple Python Dictionary for storage, But it can easily be swapped in with a real database solution, should you need to. But for the sake of simplicity, I've decided to go with a simple List of Dictionaries

Lets get started. First declare a List containing several Dictionaries, representing individual 'Book' elements

# A List of Dicts to store all of the books
books = [{
    "id": 1,
    "title": "Zero to One",
    "author": "Peter Thiel",
    "length": 195,
    "rating": 4.17
},
    {
    "id": 2,
    "title": "Atomic Habits ",
    "author": "James Clear",
    "length": 319,
    "rating": 4.35
}
]

Next, Set up a Dictionary to determine the schema for the book object, as expected by the API. This will help with validating requests later.

# Schema For the Book Request JSON
bookFields = {
    "id": fields.Integer,
    "title": fields.String,
    "author": fields.String,
    "length": fields.Integer,
    "rating": fields.Float
}

Flask Restful is an extension to the Flask Microframework that makes it a whole lot easier to build RESTful APIs

A fundamental building block provided by flask-restful is Resources. Resources give you easy access to multiple HTTP methods just by defining the methods on your Resource Class.

Here is the Book Resource class: This contains the HTTP routes for accessing, modifying and deleting each individual book entity. Now, Lets Break this down...

# Resource: Individual Book Routes
class Book(Resource):
    def __init__(self):
        # Initialize The Flsak Request Parser and add arguments as in an expected request
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument("title", type=str, location="json")
        self.reqparse.add_argument("author", type=str, location="json")
        self.reqparse.add_argument("length", type=int, location="json")
        self.reqparse.add_argument("rating", type=float, location="json")

        super(Book, self).__init__()

    # GET - Returns a single book object given a matching id
    def get(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        return{"book": marshal(book[0], bookFields)}

    # PUT - Given an id
    def put(self, id):
        book = [book for book in books if book['id'] == id]

        if len(book) == 0:
            abort(404)

        book = book[0]

        # Loop Through all the passed agruments
        args = self.reqparse.parse_args()
        for k, v in args.items():
            # Check if the passed value is not null
            if v is not None:
                # if not, set the element in the books dict with the 'k' object to the value provided in the request.
                book[k] = v

        return{"book": marshal(book, bookFields)}

        # Delete - Given an id
    def delete(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        books.remove(book[0])

        return 201

First, In the init method of the class, you initialize the request parser. It'll allow you easy access to any variable on the flask.request and also validates the response based on the arguments provided

class Book(Resource):
    def __init__(self):
        # Initialize The Flsak Request Parser and add arguments as in an expected request
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument("title", type=str, location="json")
        self.reqparse.add_argument("author", type=str, location="json")
        self.reqparse.add_argument("length", type=int, location="json")
        self.reqparse.add_argument("rating", type=float, location="json")

        super(Book, self).__init__()

Next, The GET Method, This one's really simple, it takes in an id and loops through the books list and checks each element's id with the specified ID, If a match is found, it returns that dict. The marshal method just makes sure the object that is being returned is being filtered through the fields defined in the bookFields dict.

# GET - Returns a single book object given a matching id
    def get(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        return{"book": marshal(book[0], bookFields)}

The Put method is used to update the element with the specified id, It takes a response object with the fields to be updated First, It loops through the books list and checks each element's id with the specified ID, If a match is found, It parses all of the provided arguments using the reqparser.

Then, Loops through the parsed arguments and updates the fields as in the request object.

# PUT - Given an id
    def put(self, id):
        book = [book for book in books if book['id'] == id]

        if len(book) == 0:
            abort(404)

        book = book[0]

        # Loop Through all the passed agruments
        args = self.reqparse.parse_args()
        for k, v in args.items():
            # Check if the passed value is not null
            if v is not None:
                # if not, set the element in the books dict with the 'k' object to the value provided in the request.
                book[k] = v

        return{"book": marshal(book, bookFields)}

The Delete method just simply takes in an id and deletes the element in the books list with the id.

# Delete - Given an id
    def delete(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        books.remove(book[0])

        return 201

Next is the BookList class, This contains the routes dealing with operations on the entire database.

class BookList(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            "title", type=str, required=True, help="The title of the book must be provided", location="json")
        self.reqparse.add_argument(
            "author", type=str, required=True, help="The author of the book must be provided", location="json")
        self.reqparse.add_argument("length", type=int, required=True,
                                   help="The length of the book (in pages)", location="json")
        self.reqparse.add_argument(
            "rating", type=float, required=True, help="The rating must be provided", location="json")

    def get(self):
        return{"books": [marshal(book, bookFields) for book in books]}

    def post(self):
        args = self.reqparse.parse_args()
        book = {
            "id": books[-1]['id'] + 1 if len(books) > 0 else 1,
            "title": args["title"],
            "author": args["author"],
            "length": args["length"],
            "rating": args["rating"]
        }

        books.append(book)
        return{"book": marshal(book, bookFields)}, 201

The Init method initializes the request parser. It parses the request JSON Object and also validates it based on the arguments provided.

def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            "title", type=str, required=True, help="The title of the book must be provided", location="json")
        self.reqparse.add_argument(
            "author", type=str, required=True, help="The author of the book must be provided", location="json")
        self.reqparse.add_argument("length", type=int, required=True,
                                   help="The length of the book (in pages)", location="json")
        self.reqparse.add_argument(
            "rating", type=float, required=True, help="The rating must be provided", location="json")

The Get method simply returns all the elements in the books list

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

def get(self):
        return{"books": [marshal(book, bookFields) for book in books]}

The Post method takes a JSON Object. Parses it , creates a new dict and appends it to the Books list.

def post(self):
        args = self.reqparse.parse_args()
        book = {
            "id": books[-1]['id'] + 1 if len(books) > 0 else 1,
            "title": args["title"],
            "author": args["author"],
            "length": args["length"],
            "rating": args["rating"]
        }

        books.append(book)
        return{"book": marshal(book, bookFields)}, 201

And Thats it! Now just attach those Resource classes to some endpoints and test it out!

api.add_resource(BookList, "/books")
api.add_resource(Book, "/books/<int:id>")

if __name__ == "__main__":
    app.run(debug=True)

Deploy on Docker:

Alright, Now lets deploy this microservice API to a docker container.

First, create a Dockerfile in the project directory.

A Docker file is in essence a set of instructions for building an Image which is a blueprint which your container will run off of.

Lets go over this line by line.

FROM python:3.8

RUN pip3 install pipenv

ENV PROJECT_DIR /usr/src/flaskbookapi

WORKDIR ${PROJECT_DIR}

COPY Pipfile .
COPY Pipfile.lock .
COPY . .

RUN pipenv install --deploy --ignore-pipfile

EXPOSE 5000

CMD ["pipenv", "run", "python", "api.py"]

This use the python 3.8 image from Docker Hub as our base image, This makes sure we have Python and all of its dependencies on the container

FROM python:3.8

This installs pipenv on the container.

RUN pip3 install pipenv

The first line sets a environment variable as a path to a directory to store the projects code.

Then the next command sets that directory as the working directory.

ENV PROJECT_DIR /usr/src/flaskbookapi

WORKDIR ${PROJECT_DIR}

Next, We copy over all of our files to the container

COPY Pipfile .
COPY Pipfile.lock .
COPY . .

This command runs pipenv install with the —deploy flag and also sets it to ignore the pipfile and just use the pipfile.lock to install dependencies

RUN pipenv install --deploy --ignore-pipfile

Next, We expose the 5000 port to be able to use it.

EXPOSE 5000

Finally, we run our program using the CMD command.

CMD ["pipenv", "run", "python", "api.py"]

Now lets use that Dockerfile to build an image which will later be used to make a container

docker build -t flaskbookapi:1.0 .

Now, finally! Fire up a container with the image we just built!

docker run -p 5000:5000 --name FlaskBookAPI flaskbookapi:1.0

And Boom! Out API is up and running! On a Docker container

$ docker run -p 5000:5000 --name FlaskBookAPI flaskbookapi:1.0
 * Serving Flask app "api" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Download the Project from GitHub:

https://github.com/SwarnimWalavalkar/rest-api-microservice-docker

Further Reading:

Flask Documentation: https://flask.palletsprojects.com/en/1.1.x/

Flast-Restful Documentation: https://flask-restful.readthedocs.io/en/latest/

Docker Documentation: https://docs.docker.com/

Pipenv: https://pipenv-fork.readthedocs.io/en/latest/

[출처] https://dev.to/swarnimwalavalkar/build-and-deploy-a-rest-api-microservice-with-python-flask-and-docker-5c2d

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
20 [visual c++] 오류 D8016 '/ZI'과(와) '/Gy-' 명령줄 옵션이 호환되지 않습니다. file 졸리운_곰 2023.11.19 322
19 [visual studio] Bring Your MFC Application to the Web mfc 어플리케인 web 구동 file 졸리운_곰 2023.03.20 279
18 [visual studio] [borland c] Using the WinBGIm Graphics Library with Visual Studio 2005/2008 2010 file 졸리운_곰 2023.02.05 315
17 [Visual Studio][boost c++] [C++] boost 설치 및 visual studio 설정 file 졸리운_곰 2021.11.18 464
16 winbgi Borland Turbo C routines in MS Visual C++ file 졸리운_곰 2017.03.04 290
15 VS 프로젝트 이름 변경 졸리운_곰 2017.02.12 355
14 Retrieving CPU Load Percent total in Windows with C++ 졸리운_곰 2016.12.31 487
13 Cpu 사용율 알아내는 소스 졸리운_곰 2016.12.31 471
12 [MFC] Find local network hosts ip, 로컬네트워크의 IP 주소 알아내기 졸리운_곰 2014.08.27 766
11 (Unicode) CString <==> const char* 졸리운_곰 2014.08.27 640
10 Creating a Child Process with Redirected Input and Output 졸리운_곰 2014.08.17 573
9 C++로 짠 간단한 윈도우 서비스 file 졸리운_곰 2014.05.27 1003
8 VC++ 파일에서 한 라인씩 읽기 졸리운_곰 2014.03.02 1199
7 Search Title Of Opened Windows file 졸리운_곰 2014.02.27 978
6 MFC VC++ 파일의 존재유무 체크 졸리운_곰 2014.02.18 1587
5 MFC VC++ : windows tcpip socket Send() function example 졸리운_곰 2014.02.18 1593
4 Check whether one specific process is running on windows with C++ 졸리운_곰 2014.02.18 1099
3 VC++ : Win32 console App run as to be hide (background) 졸리운_곰 2014.02.18 1245
2 VC++ : Win32 console App run as to be hide (background) 졸리운_곰 2014.02.18 1256
1 VC++ : windows : TCPIP SOCKET SERVER and CLIENT 졸리운_곰 2014.02.18 1097
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED