使用静态数据创建REST API的最佳工具

时间:2019-06-27 02:21:02

标签: mongodb rest flask

我需要一些技巧来构建具有约35000个静态(不变)JSON数据的REST API。

这是我第一次认真构建REST API,因此我需要一些设计决策建议。

首先,由于我熟悉Flask和MongoDB来存储数据,因此我打算使用Flask构建API。但是我听说MongoDB对于数据不变的不是一个很好的选择。

我想知道的是:

  1. 哪种DB适用于此类数据?
  2. 如果我希望许多用户同时使用API​​,Flask是一个不错的选择吗?
  3. 执行此操作的简要步骤是什么?我现在心中的想法如下:

步骤:

1) Upload my data to DB
2) Create a REST API that helps the user fetch the data
3) Upload the REST API to some server
4) Test with Postman to see if it works

我的总体思路正确吗?

任何建议都会很棒。预先感谢。

1 个答案:

答案 0 :(得分:2)

如果您不确定要使用哪个数据库,我会选择PostgreSQL。它具有可伸缩性,因此如果您需要在数据集上构建,它将可以正常工作。在性能方面,它仅取决于它收到的请求数量,但是我敢打赌,它可以处理您提出的任何要求。

关于API,如果您设置了Flask,那么我建议使用Flask-Restful软件包。使用ORM在名为models.py的文件中概述数据库。在名为资源的文件夹中,制作用作API资源的文件。例子是blogposts.py,它对所有或单个帖子有一个get请求,对单个帖子有一个post,post,put和delete。这是我真正的轻量级博客的内容。使用peewee作为ORM,并使用另一个名为Flask-HTTPAuth的软件包进行身份验证。

# blogposts.py

import json

from flask import jsonify, Blueprint, abort, make_response
from flask_restful import (Resource, Api, reqparse, inputs, fields,
                               url_for, marshal, marshal_with)

from auth import auth
import models

blogpost_fields = {
    'id': fields.Integer,
    'title': fields.String,
    'content': fields.String,
    'created': fields.DateTime
}

def blogpost_or_404(id):
    try:
        blogpost = models.BlogPost.get(models.BlogPost.id==id)
    except models.BlogPost.DoesNotExist:
        abort(404)
    else:
        return blogpost


class BlogPostList(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            'title',
            required=True,
            help='No title provided',
            location=['form', 'json']
        )
        self.reqparse.add_argument(
            'content',
            required=False,
            nullable=True,
            location=['form', 'json'],
            default=''
        )
        super().__init__()

    def get(self):
        blogpost = [marshal(blogpost, blogpost_fields)
            for blogpost in models.BlogPost.select()]
        return {'BlogPosts': blogpost}

    @marshal_with(blogpost_fields)
    @auth.login_required
    def post(self):
        args = self.reqparse.parse_args()
        blogpost = models.BlogPost.create(**args)
        return (blogpost, 201, {
                'Location': url_for('resources.blogposts.blogpost', id=blogpost.id)
               })

class BlogPost(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            'title',
            required=False,
            help='No title provided',
            location=['form', 'json']
        )
        self.reqparse.add_argument(
            'content',
            required=False,
            nullable=True,
            location=['form', 'json'],
            default=''
        )
        super().__init__()

    @marshal_with(blogpost_fields)
    def get(self, id):
        return (blogpost_or_404(id))

    @marshal_with(blogpost_fields)
    @auth.login_required
    def put(self, id):
        args = self.reqparse.parse_args()
        try:
            blogpost = models.BlogPost.select().where(
                models.BlogPost.id==id).get()
        except models.BlogPost.DoesNotExist:
            return make_response(json.dumps(
                    {'error': 'That blogpost does not exist or is not editable'}
                ), 403)
        else:
            query = blogpost.update(**args).where(models.BlogPost.id==id)
            query.execute()
            blogpost = (blogpost_or_404(id))
            return (blogpost, 200, {
                'Location': url_for('resources.blogposts.blogpost', id=id)
               })

    @auth.login_required
    def delete(self, id):
        try:
            blogpost = models.BlogPost.select().where(
                models.BlogPost.id==id).get()
        except models.BlogPost.DoesNotExist:
            return make_response(json.dumps(
                    {'error': 'That blogpost does not exist or is not editable'}
                ), 403)
        else:
            query = blogpost.delete().where(models.BlogPost.id==id)
            query.execute()
            return '', 204, {'Location': url_for('resources.blogposts.blogposts')}


blogposts_api = Blueprint('resources.blogposts', __name__)
api = Api(blogposts_api)
api.add_resource(
    BlogPostList,
    '/blogposts',
    endpoint='blogposts'
)
api.add_resource(
    BlogPost,
    '/blogposts/<int:id>',
    endpoint='blogpost'
)

资源类具有带有http方法名称的方法,这是设置允许哪些方法的方法。例如,如果我尝试删除没有ID的/ blogposts,它将以不允许的方法进行响应。删除仅针对单个帖子定义。封送处理可确定响应中包含哪些信息,您可以在顶部使用blogpost_fields对其进行定义。在每个类的 init 中,我们定义了请求解析器,该请求解析器确定API需要的信息。在此示例中,我们仅需要标题和帖子内容。在用户资源中,您可以添加电子邮件,用户名,密码,验证密码,管理员状态等信息。

# models.py

import datetime

import jwt
from argon2 import PasswordHasher
from peewee import *

import config

DATABASE = PostgresqlDatabase('blogdb', user=config.DB['USER'], password=config.DB['PW'], host=config.DB['HOST'])
HASHER = PasswordHasher()


class User(Model):
    username = CharField(unique=True)
    email = CharField(unique=True)
    password = CharField()

    class Meta:
        database = DATABASE

    @classmethod
    def create_user(cls, username, email, password, **kwargs):
        email = email.lower()
        try:
            cls.select().where(
                (cls.email==email)|(cls.username**username)
            ).get()
        except cls.DoesNotExist:
            user = cls(username=username, email=email)
            user.password = user.set_password(password)
            user.save()
            return user
        else:
            raise Exception("User with that email or username already exists")

    @staticmethod
    def verify_auth_token(token):
        try:
            payload = jwt.decode(token, config.SECRET_KEY)
            return payload['sub']
        except jwt.ExpiredSignatureError:
            return 'Signature expired. Please log in again.'
        except jwt.InvalidTokenError:
            return 'Invalid token. Please log in again.'

    @staticmethod
    def set_password(password):
        return HASHER.hash(password)

    def verify_password(self, password):
        return HASHER.verify(self.password, password)

    def generate_auth_token(self, id):
        try:
            payload = {
                'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0, seconds=5),
                'iat': datetime.datetime.utcnow(),
                'sub': id
            }
            return jwt.encode(
                payload,
                config.SECRET_KEY,
                algorithm='HS256'
            )
        except Exception as e:
            return e


class BlogPost(Model):
    title = CharField(default='', unique=True)
    content = TextField(default='')
    created = DateTimeField(default=datetime.datetime.now)

    class Meta:
        database = DATABASE

def initialize():
    DATABASE.connect()
    DATABASE.create_tables([User, BlogPost], safe=True)
    DATABASE.close()
# auth.py

from flask import g

from flask_httpauth import HTTPTokenAuth

import models


auth = HTTPTokenAuth(scheme='Bearer')

@auth.verify_token
def verify_token(token):
    user = models.User.verify_auth_token(token)
    if user is not None:
        g.user = user
        return True
    return False

如果您曾经使用过像SQLAlchemy这样的ORM,那么模型是不言自明的。我建议使用该软件包,因为您的数据集远比本示例中的数据集大得多。 HTTPAuth允许您使用必需的身份验证方法来修饰API资源方法。在我的示例中,登录将生成一个JWT,该JWT需要与每个请求一起作为Bearer令牌发送。

所有设置完成后,您就可以在app.py中注册API蓝图

# app.py

app = Flask(__name__)
app.register_blueprint(users_api, url_prefix='/api/v1')
app.register_blueprint(blogposts_api, url_prefix='/api/v1')
app.register_blueprint(login_api)

就是这样!