如何构造JSON模式以验证DynamoDB和RESTAPI?

时间:2018-12-11 18:46:29

标签: python json rest amazon-dynamodb marshmallow

我正在编写一个REST API,它将几个复杂的对象存储到AWS DynamoDB,然后在被请求时检索它们,对它们执行计算并返回结果。这是大量提取,简化,重命名的伪代码。

class Widget:
    def __init__(self, height, weight):
        self.height = height
        self.weight = weight

class Machine:
    def __init__ (self, widgets):
        self.widgets = widgets
    def useful_method ():
        return "something great"

class WidgetSchema (Schema):
    height = fields.Decimal()
    weight = fields.Decimal()
    @post_load
    def make_widget (self, data):
        return Widget(*data)

class MachineSchema (Schema):
    widgets = fields.List(fields.Nested(WidgetSchema))
    def make_machine (self, data):
        return Machine(*data)

app = Flask(__name__)
dynamodb = boto3.resource("dynamodb", ...) 

@app.route("/machine/<uuid:machine_id>", methods=['POST'])
def create_machine(machine_id):
    input_json = request.get_json()
    validated_input = MachineSchema().load(input_json)
    # NOTE: validated_input should be a Python dict which
    # contains Decimals instead of floats, for storage in DynamoDB.
    validate_input['id'] = machine_id
    dynamodb.Table('machine').put_item(Item=validate_input)
    return jsonify({"status", "success", error_message = ""})

@app.route("/machine/<uuid:machine_id>/compute", methods=['GET'])
def get_machine(machine_id):
    result = dynamodb.Table('machine').get_item(Key=machine_id)
    return jsonify(result['Item'])

@app.route("/machine/<uuid:machine_id>/compute", methods=['GET'])
def compute_machine(machine_id):
    result = dynamodb.Table('machine').get_item(Key=machine_id)
    validated_input = MachineSchema().load(result['Item'])
    # NOTE: validated_input should be a Machine object
    # which has made use of the post_load
    return jsonify(validated_input.useful_method())

与此相关的问题是,我需要让棉花糖模式执行双重任务。首先,在create_machine函数中,我需要一个模式来确保调用我的REST API的用户已经向我传递了一个格式正确的对象,没有多余的字段并满足所有必填字段,等等。我需要确保我没有存储毕竟数据库中有无效的垃圾。它还需要递归地爬行输入的JSON并将所有JSON值转换为正确的类型。例如,Dynamo不支持浮点数,因此它们必须是十进制,如下所示。这是棉花糖使非常容易的事情。如果没有post_load,那么正是将其作为validated_input产生。

该模式的第二项工作是需要它采取从DynamoDB中检索的Python对象,该对象看上去几乎就像用户输入的JSON一样,除了浮点数是小数,然后进行翻译并将其放入我的Python对象Machine和Widget中。这是我需要再次读取对象的地方,但这一次使用后加载创建对象。但是,在这种情况下,我不希望我的数字是小数。我希望它们是标准的Python浮点数。

我可以为此编写两个完全不同的棉花糖模式,并用它来完成。一个人的身高和体重将为小数,而另一个将为浮点数。一个将对每个对象进行后期加载,而一个将不进行后期加载。但是编写两个相同的模式是一个巨大的痛苦。我的架构定义有几百行。用后加载继承数据库版本似乎不是正确的方向,因为我需要更改任何字段。嵌套指向正确的类。例如,即使我从MachineSchema继承了MachineSchemaDBVersion,并添加了post_load,MachineScehemaDBVersion仍将引用WidgetScehema,而不是WidgetSchema的某些数据库版本,除非我也使Widgets领域也有所发展。

我可能会派生自己的Schema对象,并为我们是否处于DB模式下传递一个标志。

人们通常如何处理希望将REST API输入或多或少直接存储到DynamoDB并进行一些验证,然后在以后使用该数据构造Python对象进行计算的问题?

我尝试过的方法是让我的模式始终实例化我的Python对象,然后使用来自完全构造对象的转储将它们哑化到数据库中。问题在于,在我的示例Machine或Widget中,计算库的对象没有我需要存储在数据库中的所有必填字段,例如ID或名称或说明。这些对象是专门为进行计算而制作的。

1 个答案:

答案 0 :(得分:1)

我最终找到了解决方案。实际上,我所做的是专门生成棉花糖架构,以将其从DynamoDB转换为Python对象。所有Schema类都具有@post_load方法,这些方法可以转换为Python对象,并且所有字段都标记有它们在Python世界(而不是数据库世界)中所需的类型。

在验证来自REST API的输入并确保不允许不良数据进入数据库时​​,我调用MySchema().validate(input_json),检查是否没有错误,如果没有,则将input_json转储到数据库。

这只剩下一个额外的问题,那就是需要清理input_json才能输入数据库,这是我之前使用棉花糖做的。但是,也可以通过调整我的JSON解码器以从浮点数读取小数来轻松完成此操作。

因此,总而言之,我的JSON解码器正在进行递归遍历数据结构并将Float与Marshmallow分别转换为Decimal的工作。棉花糖在每个对象的字段上运行验证,但仅检查结果是否有错误。然后将原始输入转储到数据库中。

我需要添加此行以将其转换为十进制。

app.json_decoder = partial(flask.json.JSONDecoder, parse_float=decimal.Decimal)

我的创建函数现在看起来像这样。请注意,如何将由我更新后的JSON解码器解析的原始input_json直接插入数据库,而不是将棉花糖输出的任何数据合并到数据库中。

@app.route("/machine/<uuid:machine_id>", methods=['POST'])
def create_machine(machine_id):
    input_json = request.get_json() # Already ready to be DB input as is.
    errors = MachineSchema().validate(input_json)
    if errors:
      return jsonify({"status": "failure",message = dumps(errors)})
    else:
      input_json['id'] = machine_id
      dynamodb.Table('machine').put_item(Item=input_json)
      return jsonify({"status", "success", error_message = ""})