Как я могу структурировать мою схему JSON для проверки для DynamoDB и RESTAPI? - PullRequest
0 голосов
/ 11 декабря 2018

Я пишу 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())

Проблема в том, что мне нужно, чтобы моя схема Marshmallow выполняла двойную функцию.Для начала, в функции create_machine мне нужна схема, чтобы пользователь, вызывающий мой REST API, передал мне правильно сформированный объект без лишних полей и отвечающий всем необходимым полям и т. Д. Мне нужно убедиться, что я не сохраняюнедействительный мусор в БД в конце концов.Также необходимо рекурсивно сканировать входной JSON и преобразовать все значения JSON в правильный тип.Например, числа с плавающей запятой не поддерживаются в Динамо, поэтому они должны быть десятичными, как показано здесь.Это то, что Зефир сделать довольно легко.Если бы не было post_load, это было бы именно то, что было бы получено как validated_input.

Второе задание схемы заключается в том, что ему нужно взять объект Python, извлеченный из DynamoDB, который выглядит почти точно так же, как пользовательский ввод JSON, за исключением числа с плавающей запятой, представляет собой десятичные дроби, и переводит его в мои объекты Python, Machine и Widget.Здесь мне нужно снова прочитать объект, но на этот раз использовать пост-загрузку для создания объектов.В этом случае, однако, я не хочу, чтобы мои числа были десятичными.Я бы хотел, чтобы они были стандартными поплавками Python.

Я мог бы написать для этого две совершенно разные схемы Marshmallow и, конечно же, покончить с этим.Можно иметь десятичные дроби для роста и веса, а можно просто плавать.Можно было бы загрузить сообщения для каждого объекта, а другого - нет.Но написание двух одинаковых схем - огромная боль.Мои определения схемы имеют длину в несколько сотен строк.Наследование версии БД с последующей загрузкой не выглядело как правильное направление, потому что мне нужно было бы изменить любые поля. Вложенные, чтобы указывать на правильный класс.Например, даже если бы я унаследовал MachineSchemaDBVersion от MachineSchema и добавил post_load, MachineScehemaDBVersion все равно будет ссылаться на WidgetScehema, а не на некоторую версию DB WidgetSchema, если только я не перенесу поле виджетов также.

Я мог бы потенциально получить свой собственныйОбъект схемы и флаг передают, находимся ли мы в режиме БД или нет.

Как люди обычно решают эту проблему, желая более или менее сохранять входные данные API REST непосредственно в DynamoDB с некоторой проверкой, а затем использовать этоданные позже для создания объектов Python для вычислений?

В методе, который я попробовал, моя схема всегда создает экземпляры моих объектов Python, а затем выводит их в базу данных, используя дампы из полностью сконструированного объекта.Проблема заключается в том, что объекты вычислительной библиотеки, в моем примере Machine или Widget, не имеют всех обязательных полей, которые мне нужно хранить в базе данных, таких как идентификаторы, имена или описания.Объекты созданы специально для вычислений.

1 Ответ

0 голосов
/ 13 декабря 2018

Я закончил тем, что нашел решение для этого.По сути, я сделал генерацию схемы Marshmallow исключительно для перевода из DynamoDB в объекты Python.Все классы Schema имеют методы @post_load, которые преобразуются в объекты Python, и все поля помечены тем типом, которым они должны быть в мире Python, а не в мире баз данных.

При проверке входных данных из REST API иУбедившись, что в базу данных не попадают никакие неверные данные, я вызываю MySchema().validate(input_json), проверяю, нет ли ошибок, и, если нет, сбрасываю input_json в базу данных.

Это оставляет только одну дополнительнуюПроблема в том, что input_json нужно очистить для входа в базу данных, что я ранее делал с Marshmallow.Однако это также легко сделать, настроив мой JSON-декодер для чтения десятичных чисел из чисел с плавающей запятой.

Итак, в общем, мой JSON-декодер выполняет рекурсивную прогулку по структуре данных и преобразовывает Float в десятичную отдельно от Marshmallow.,Зефир выполняет проверку на полях каждого объекта, но результаты проверяются только на наличие ошибок.Исходный ввод затем выгружается в базу данных.

Мне нужно было добавить эту строку, чтобы выполнить преобразование в десятичное число.

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

Моя функция создания теперь выглядит следующим образом.Обратите внимание, как исходный input_json, проанализированный моим обновленным JSON-декодером, вставляется непосредственно в базу данных, а не в любой вывод данных из Marshmallow.

@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 = ""})
...