Xây base project api bằng flask

Bài viết này mình sẽ hướng dẫn các bạn xây dựng một base project api bằng python sử dụng framework Flask, project đồng thời sẽ sinh ra document cho api luôn

Cài đặt

Đầu tiền kiểm tra xem máy bạn đã cài đặt pip chưa:

pip --version

Nếu chưa cài đặt bạn có thể xem link này để cài đặt pip vào máy của bạn: https://pip.pypa.io/en/latest/installing/

Cài đặt python và virtualenv :

sudo apt-get install python3-pip
sudo pip install virtualenv
sudo pip3 install virtualenvwrapper

Build base project

Tạo thư mục

mkproject base_flask_api

Trong thư mục vừa tạo bạn tạo các folder vào init file theo cấp sau:

.
├── app
│   ├── __init__.py
│   ├── main
│   │   ├── controller
│   │   │   └── __init__.py
│   │   ├── __init__.py
│   │   ├── model
│   │   │   └── __init__.py
│   │   └── service
│   │       └── __init__.py
│   └── test
│       └── __init__.py
└── requirements.txt

Trong đó file __init__.py bạn để trống, file này để cho flask hiểu rằng nó là 1 module thôi

Tiếp theo cài đặt flask framework và các package cần thiết

pip install flask-bcrypt

pip install flask-restplus

pip install Flask-Migrate

pip install pyjwt

pip install Flask-Script

pip install flask_testing

sau khi cài xong cần update file requirements.txt để lưu lại những package đã cài đặt

pip freeze > requirements.txt

Sau khi chạy lệnh thì requirements.txt sẽ có nội dung thế này

alembic==0.9.8
aniso8601==3.0.0
bcrypt==3.1.4
cffi==1.11.5
click==6.7
Flask==0.12.2
Flask-Bcrypt==0.7.1
Flask-Migrate==2.1.1
flask-restplus==0.10.1
Flask-Script==2.0.6
Flask-SQLAlchemy==2.3.2
Flask-Testing==0.7.1
itsdangerous==0.24
Jinja2==2.10
jsonschema==2.6.0
Mako==1.0.7
MarkupSafe==1.0
pycparser==2.18
PyJWT==1.6.0
python-dateutil==2.7.0
python-editor==1.0.3
pytz==2018.3
six==1.11.0
SQLAlchemy==1.2.5
Werkzeug==0.14.1

Trong folder main tạo 1 file config.py với nội dung như sau:

import os

# uncomment the line below for postgres database url from environment variable
# postgres_local_base = os.environ['DATABASE_URL']

basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'my_precious_secret_key')
    DEBUG = False


class DevelopmentConfig(Config):
    # uncomment the line below to use postgres
    # SQLALCHEMY_DATABASE_URI = postgres_local_base
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_main.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class TestingConfig(Config):
    DEBUG = True
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_test.db')
    PRESERVE_CONTEXT_ON_EXCEPTION = False
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class ProductionConfig(Config):
    DEBUG = False
    # uncomment the line below to use postgres
    # SQLALCHEMY_DATABASE_URI = postgres_local_base


config_by_name = dict(
    dev=DevelopmentConfig,
    test=TestingConfig,
    prod=ProductionConfig
)

key = Config.SECRET_KEY

File này để setting môi trường và database, trong phần này database mặc định là sqlite, nếu bạn sử dụng mysql hay postgree thì cần cài đặt thêm packages PyMySQL hoặc psycopg2. rồi chỗ dòng SQLALCHEMY_DATABASE_URI bạn cần thay đổi nội dung cho nó, giả sử mysql sẽ đổi thành SQLALCHEMY_DATABASE_URI=mysql+pymysql://root:[email protected]/flask_test (root là users, 123456 là password, localhost là host url, flask_test là tên của database), postgree cũng tương tự chỉ cần đổi mysql+pymysql thành postgresql+psycopg2 là được.

Trong thư mục main bạn thêm nội dung sau vào file __init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt

from .config import config_by_name

db = SQLAlchemy()
flask_bcrypt = Bcrypt()


def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config_by_name[config_name])
    db.init_app(app)
    flask_bcrypt.init_app(app)

    return app

Trong thư mục gỗ của project bạn tạo 1 file có tên là manage.py có nội dung như sau:

import os
import unittest

from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager

from app.main import create_app, db

app = create_app(os.getenv('BOILERPLATE_ENV') or 'dev')

app.app_context().push()

manager = Manager(app)

migrate = Migrate(app, db)

manager.add_command('db', MigrateCommand)

@manager.command
def run():
    app.run()

@manager.command
def test():
    """Runs the unit tests."""
    tests = unittest.TestLoader().discover('app/test', pattern='test*.py')
    result = unittest.TextTestRunner(verbosity=2).run(tests)
    if result.wasSuccessful():
        return 0
    return 1

if __name__ == '__main__':
    manager.run()

Cuối cùng chạy lệnh sau để khởi tạo server python

python manage.py run

mỗi mọi thứ đều ổn nó sẽ show thế này

Như vậy chúng ta đã xong phần create app cho flask, tiếp theo sẽ xử lý đến các phần tiếp theo như model, controller, service hay unit test

Database Models and Migration

Trong package model bạn tạo 1 file user.py có nội dung sau

from .. import db, flask_bcrypt

class User(db.Model):
    """ User Model for storing user related details """
    __tablename__ = "user"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    email = db.Column(db.String(255), unique=True, nullable=False)
    registered_on = db.Column(db.DateTime, nullable=False)
    admin = db.Column(db.Boolean, nullable=False, default=False)
    public_id = db.Column(db.String(100), unique=True)
    username = db.Column(db.String(50), unique=True)
    password_hash = db.Column(db.String(100))

    @property
    def password(self):
        raise AttributeError('password: write-only field')

    @password.setter
    def password(self, password):
        self.password_hash = flask_bcrypt.generate_password_hash(password).decode('utf-8')

    def check_password(self, password):
        return flask_bcrypt.check_password_hash(self.password_hash, password)

    def __repr__(self):
        return "<User '{}'>".format(self.username)

Trong file manage.py bạn cần import file user.py này vào để có thể chạy migration

...
from app.main.model import user
...

Tiếp theo chạy migration

python manage.py db init
python manage.py db migrate --message 'initial database migration'
python manage.py db upgrade

Test

Trong module test bạn tạo một file có nội dung như sau:

import os
import unittest

from flask import current_app
from flask_testing import TestCase

from manage import app
from app.main.config import basedir


class TestDevelopmentConfig(TestCase):
    def create_app(self):
        app.config.from_object('app.main.config.DevelopmentConfig')
        return app

    def test_app_is_development(self):
        self.assertFalse(app.config['SECRET_KEY'] is 'my_precious')
        self.assertTrue(app.config['DEBUG'] is True)
        self.assertFalse(current_app is None)
        self.assertTrue(
            app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_main.db')
        )


class TestTestingConfig(TestCase):
    def create_app(self):
        app.config.from_object('app.main.config.TestingConfig')
        return app

    def test_app_is_testing(self):
        self.assertFalse(app.config['SECRET_KEY'] is 'my_precious')
        self.assertTrue(app.config['DEBUG'])
        self.assertTrue(
            app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_test.db')
        )


class TestProductionConfig(TestCase):
    def create_app(self):
        app.config.from_object('app.main.config.ProductionConfig')
        return app

    def test_app_is_production(self):
        self.assertTrue(app.config['DEBUG'] is False)


if __name__ == '__main__':
    unittest.main()

File này có mục đích test file config bạn vừa tạo ở phía trên

Chạy test bằng lệnh sau: python manage.py test

Controller

Trong folder service bạn tạo mới một file user_service.py có nội dung như sau:

import uuid
import datetime

from app.main import db
from app.main.model.user import User


def save_new_user(data):
    user = User.query.filter_by(email=data['email']).first()
    if not user:
        new_user = User(
            public_id=str(uuid.uuid4()),
            email=data['email'],
            username=data['username'],
            password=data['password'],
            registered_on=datetime.datetime.utcnow()
        )
        save_changes(new_user)
        response_object = {
            'status': 'success',
            'message': 'Successfully registered.'
        }
        return response_object, 201
    else:
        response_object = {
            'status': 'fail',
            'message': 'User already exists. Please Log in.',
        }
        return response_object, 409


def get_all_users():
    return User.query.all()


def get_a_user(public_id):
    return User.query.filter_by(public_id=public_id).first()


def save_changes(data):
    db.session.add(data)
    db.session.commit()

Phần service này khá đơn giản, nó có các hàm như create users, get all users hay get detail một users

Trong package main bạn tạo mới một package tên là util

trong util bạn tạo một file là dto.py có nội dung như sau:

from flask_restplus import Namespace, fields


class UserDto:
    api = Namespace('user', description='user related operations')
    user = api.model('user', {
        'email': fields.String(required=True, description='user email address'),
        'username': fields.String(required=True, description='user username'),
        'password': fields.String(required=True, description='user password'),
        'public_id': fields.String(description='user Identifier')
    })

để hiểu hơn về DTO là gì bạn tham khảo link sau https://en.wikipedia.org/wiki/Data_transfer_object File này sẽ giúp ta response api, validate hay viết docs

Tạo user_controller.py trong controller

from flask import request
from flask_restplus import Resource

from ..util.dto import UserDto
from ..service.user_service import save_new_user, get_all_users, get_a_user

api = UserDto.api
_user = UserDto.user


@api.route('/')
class UserList(Resource):
    @api.doc('list_of_registered_users')
    @api.marshal_list_with(_user, envelope='data')
    def get(self):
        """List all registered users"""
        return get_all_users()

    @api.response(201, 'User successfully created.')
    @api.doc('create a new user')
    @api.expect(_user, validate=True)
    def post(self):
        """Creates a new User """
        data = request.json
        return save_new_user(data=data)


@api.route('/<public_id>')
@api.param('public_id', 'The User identifier')
@api.response(404, 'User not found.')
class User(Resource):
    @api.doc('get a user')
    @api.marshal_with(_user)
    def get(self, public_id):
        """get a user given its identifier"""
        user = get_a_user(public_id)
        if not user:
            api.abort(404)
        else:
            return user

Controller này sẽ function get list users hay get detail users, đồng thời cũng có những phần để viết docs cho api luôn trong code, cái này lúc ban đầu nhìn có vẻ hơi rối code một chút.

Thêm vào file __init__.py bên trong app nội dung sau:

# app/__init__.py

from flask_restplus import Api
from flask import Blueprint

from .main.controller.user_controller import api as user_ns

blueprint = Blueprint('api', __name__)

api = Api(blueprint,
          title='FLASK RESTPLUS API BOILER-PLATE WITH JWT',
          version='1.0',
          description='a boilerplate for flask restplus web service'
          )

api.add_namespace(user_ns, path='/user')

file này như kiểu đăng ký route cho các api sử dụng packages blueprint

bạn cần update file manage.py với nội dung sau để đăng ký route

from app import blueprint
...

app = create_app(os.getenv('BOILERPLATE_ENV') or 'dev')
app.register_blueprint(blueprint)

app.app_context().push()

...

Bạn chạy lại server: python manage.py run và vào link http://127.0.0.1:5000/ sẽ show ra docs cho api bạn vừa viết hay có thể test nhưng api get list users ở trên

Phần tiếp theo mình sẽ dịch nốt bài viết.

Tài liệu

Bài viết được dịch từ

All Rights Reserved