13g10n
На главную

Dependency Injector для Flask — валидация параметров

4 минуты

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

Позволю себе на секунду отойти от реализации и сказать пару слов про неочевидный недостаток фреймворка. Речь идёт про глобальную переменную request, которая очень нравится новичкам, но своим существованием подталкивает делать не совсем правильные (а иногда и совсем неправильные) вещи. Безусловно, с точки зрения бездумного накидывания кода иметь возможность доставать данные из запроса на любом из слоёв приложения очень удобно! Но само наличие этой возможности позволяет другим разработчикам из вашей команды (вы бы никогда на такую глупость не пошли, я знаю) так испортить поток данных условными хот-фиксами в конкретных местах, что разгребать придётся в несколько лопат и в противогазах!

Flask не принуждает (и даже не подталкивает) нас к какой-то организованной валидации входных данных — про это нет ни слова в официальной документации. Меж тем, нужно понимать, что организованные контракты между апи и фронтом являются не только катализаторами для ускорения и распараллеливания разработки, но ещё и позволяют отлавливать неочевидные баги в случаях, когда эндпоинт используется в нескольких местах.

Начнём с валидации URL параметров, с которыми мы разбирались раньше. Для упрощения я опущу процесс приведения типов и валидации и мы будем ожидать, что у нас просто есть условная функция validate, которая принимает значение и тип, делает всё необходимое и выдаёт либо приведенное к типу значение, либо выбрасывает кастомное исключение, которое мы можем обработать вне нашего декоратора средствами flask, чтобы выдать пользователю красивое и информативное сообщение об ошибке.

Так же, изменим немного наш код и используем цикл for вместо dict comprehension чтобы было проще расширять логику в будущем:

def filter_params(func):
    signature = inspect.signature(func)

    @wraps(func)
    def decorated_function(*args, **kwargs):
        params = {}

        for param, param_value in kwargs.items():
            if param in signature.parameters:
                params[param] = validate(param_value, signature.parameters[param].annotation)

        return func(*args, **params)

    return decorated_function

Что мы получили этим небольшим изменением? Во-первых, мы можем не использовать стандартную конвертацию типов и избежать небольшого дублирования этих самых типов (в параметрах функции и в route декораторе). Более того, теперь валидация может быть сколь угодно сложной, а ошибки более информативными — мы контролируем всё в очевидных и удобных для нас местах.

Во-вторых, посмотрите как просто мы сейчас добавим валидацию GET параметров, которые, как правило, тоже стандартизированы. Добавляем offset и limit, которые нужны для пагинации, прямо в хэндлер!

@app.route('/<lang>/pages')
@filter_params
def list_pages(lang: Language, offset: Optional[int] = 0, limit: Optional[int] = 10) -> list:
    return pages.get_list(lang, offset, limit)

И добавляем в цикл нашего декоратора дополнительные элементы прямиком из request.args:

def filter_params(func):
    signature = inspect.signature(func)

    @wraps(func)
    def decorated_function(*args, **kwargs):
        params = {}

        for param, param_value in dict(request.args, **kwargs).items():
            if param in signature.parameters:
                params[param] = validate(param_value, signature.parameters[param].annotation)

        return func(*args, **params)

    return decorated_function

Всё это начинает очень походить на FastAPI. Особенно это заметно, если мы будем использовать async хэндлеры из второй версии фласка и кастомный роут декоратор:

@app.get('/<lang>/pages')
async def list_pages(lang: Language, offset: Optional[int] = 0, limit: Optional[int] = 10) -> list:
    return pages.get_list(lang, offset, limit)

Конечно, не стоит себя обманывать. Внешне это один и тот же код, но всё же исполняться он будет двумя разными фреймворками со своими особенностями, плюсами и минусами.

Рассмотренные вчера и сегодня техники, могут быть использованы как по отдельности, так и вместе — всё зависит от ваших целей и задач. Более того, используя этот подход, можно постепенно приводить всю свою кодовую базу к единому виду, который в один день устранит зависимость от реализации конкретного фреймворка и позволит мигрировать с одного на другой максимально безболезненно.

В следующий раз мы рассмотрим валидацию request.json при помощи pydantic моделей, чтобы окончательно закрыть тему контрактов между бэк и фронт командами.

PythonFlask