Friday, February 7, 2020

REST on Python Flask for API's - 2

A simple REST API

Following up from our previous exercise on how to build a API with Flask, let's try to put together a simple REST API solution

We start by putting together a simple high-level structure
  • mkdir restapi && cd restapi
  • virtualenv venv
  • souce venv/bin/activate
  • mkdir api
  • touch app_namespace.py Dockerfile requirements.txt run.py
  • touch api/__init__.py api/player.py api/country.py api/health.py
  • pip install flask
  • pip install flask_restplus
Phew.....A lot of steps.....


open restapi directory in your favorite IDE, open app_namespace.py and create a Flask object

from flask import Flask

app = Flask(__name__)                  #  Create a Flask WSGI application

open api/__init__.py

from flask_restplus import Api
from app_namespace import app


api = Api(app)                         #  Create a Flask-RESTPlus API

What is __init__.py?

__init_.py is the first file to get executed from this directory. Think of it like a constructor to this file structure. Every directory can have a __init__.py. Before first execution of files in that directory init will get executed. In our case, we have 3 files in api directory - health, country and players. Before any invocation of api/health or api/players or api/country __init__.py will be called. All initialization common to files in this directory can/should be placed here.

open api/health.py

from flask_restplus import Resource
from api import api


ns = api.namespace('health', description='Operations related to players')


@ns.route('')                   #  Create a URL route to this resource
class Health(Resource):            #  Create a RESTful resource
    def get(self):                     #  Create GET endpoint
        return {'message': 'success'}

All that we have done is created a simple class that is derived from "Resource" and implement "get" method. If you are not familiar with Python classes, take a quick tour from here - https://realpython.com/python3-object-oriented-programming/. "get" method returns a simple JSON. If we need to return a complex JSON object, we could use something like "jsonify". We'll be using it down the line. If you are wondering why we don't have @ns.route('/health'). Reason we just have root ('/') here is because, "health namespace" is registered with "/health" path. Any route that comes from health namespace will have 'health' attached to it. That explains empty string on class Health
open api/player.py

from flask_restplus import Resource
from api import api
from flask import jsonify, request

ns = api.namespace('players', description='Operations related to players')


@ns.route('/')
class PlayerCollection(Resource):
    players = [{
        "id": 1,
        "name": "KL Rahul",
        "age": 24
    },
        {
            "id": 2,
            "name": "Shikar Dhawan",
            "age": 28
        },
        {
            "id": 3,
            "name": "Rohit Sharma",
            "age": 32
        },
        {
            "id": 4,
            "name": "Virat Kohli",
            "age": 31
        }
    ]

    def get(self):
        """Returns list of players."""
        return jsonify(self.players)

    @api.response(201, 'Player successfully created.')
    def post(self):
        """Add new player to player's collection."""
        self.players.append(request.json)
        return None, 201


@ns.route('/')
@api.response(404, 'Player not found.')
class Player(Resource):            #  Create a RESTful resource
    def get(self, id):                     #  Create GET endpoint
        pl_collection = PlayerCollection()
        for player in pl_collection.players:
            if player["id"] == id:
                return jsonify(player)

"get" and "post" carry same endpoint. But when we look at capability to locate a player by ID or name, then /health alone does not suffice. we need additional parameters to URL. This could be ID or name, like https:///api/player/ or https:///api/player/

Finally, open api/country.py. You see the process is the same. Feel free to do something same like "player" endpoint. Create a simple data structure to store countries and capital. Create "get" and "post" for root endpoint and a new class to allow for search and updates.

from flask_restplus import Resource
from api import api
ns = api.namespace('country', description='Operations related to players')

@ns.route('')                   #  Create a URL route to this resource
class Country(Resource):            #  Create a RESTful resource
    def get(self):                     #  Create GET endpoint
        return {'hello': 'country'}

All this is fine. Unless we put the endpoints together we are not going to have a FLASK solution. This is what happens in run.py. We import all namespaces from respective api's, initialize application and run it.

Blueprint is a library as a part of flask that allows for Swagger like documentation. Every endpoint will get a mention in documentation. You'll notice it when you run the application. This allows for easier API consumption from other divisions in enterprise.  You can read more about it here - https://flask.palletsprojects.com/en/1.1.x/blueprints/

Note: Application by default runs on port 5000. If you wish to run it on any other port, override run method like below

app.run(debug=True, host='0.0.0.0', port=6000)

open /run.py
from flask import Blueprint
from app_namespace import app
from api import api
from api.player import ns as player_namespace
from api.country import ns as country_namespace
from api.health import ns as health_namespace



def initialize_app(flask_app):
    # configure_app(flask_app)
    blueprint = Blueprint('api', __name__, url_prefix='/api')
    api.init_app(blueprint)
    api.add_namespace(health_namespace)
    api.add_namespace(player_namespace)
    api.add_namespace(country_namespace)
    flask_app.register_blueprint(blueprint)

if __name__ == '__main__':
    initialize_app(app)
    app.run(debug=True, host='0.0.0.0')


Next step is to add docker capabilities and run this simple Flask solution in a container. Lets see that in NEXT part

No comments: