Рубрики
Без рубрики

Объяснение инъекции зависимости Fastapi и Typer и катание на Python

Что -то, что мне действительно нравится в Fastapi и Typer, оба от одного и того же автора, Себастьяна Рамиреса, он же Ti … Tagged с Python, WebDev.

Что -то я Действительно Как о FASTAPI и Тайпер , оба от одного и того же автора, Себастьян Рамирес, он же Танголо , это супер-концентративная инъекция зависимости. В недавнем проекте я не смог использовать Fastapi, поэтому я решил свернуть свой собственный. В этом посте я опишу, как. Но сначала позвольте мне показать вам, как хорошие библиотеки Tiangolo для использования:

Тайпер для создания инструментов командной строки Python. Часто вы хотите иметь возможность вызывать сценарии Python из командной строки с дополнительными аргументами, чтобы выполнять всевозможные задачи автоматизации. Python имеет низкий уровень метода извлечения значений, передаваемых из командной линии с использованием sys.arv который содержит список аргументов:

python sys_test.py 123
import sys
print(f"Your number was {sys.argv[1]}")

Это печатает “Ваш номер был 123”

Но это становится немного сложным, когда у вас есть более одного аргумента, и особенно с дополнительными аргументами. Это также не делает ничего, чтобы помочь вам произвести ошибки, когда ожидается, что ARG не предоставляются.

Использование Argparse

Python имеет решение для этого, однако, он поставляется с библиотекой более высокого уровня для создания интерфейса командной строки (позволяя для дополнительных аргументов, справочного текста и т. Д.) Использование Аргпарсер модуль:

from argparse import ArgumentParser


def main(foo: str, bar: int):
    print(foo)
    print(bar)


if __name__ == "__main__":
    parser = ArgumentParser(description="Function that does things and stuff")
    parser.add_argument("foo", type=str, help="Required Foo")
    parser.add_argument("--bar", type=int, help="Optional Bar", default=1)

    args = parser.parse_args()

    main(args.foo, args.bar)

Как видите, Argparser работает, заставляя вас написать какой -то код, чтобы подготовить все аргументы. И дает вам возможность определить некоторую документацию для этих аргументов. Запустить его сценарий, используя python argparser_example.py -help Флаг выглядит так:

usage: argparser_example.py [-h] [--bar BAR] foo

Function that does things and stuff

positional arguments:
  foo         Required Foo

optional arguments:
  -h, --help  show this help message and exit
  --bar BAR   Optional Bar

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

К счастью, в экосистеме Python есть несколько других библиотек, некоторые из которых более удобны для использования или более мощного. Одна такая библиотека, которую я ранее использовал, – это Google Огонь В то время как другой, который я использую сейчас, это Tiangolo’s Тайпер

Используя Тайпер

Тайпер эквивалент приведенного выше кода значительно короче:

import typer

def main(
    foo: str=typer.Argument(..., help="Required Foo"),
    bar: int=typer.Argument(1, help="Optional Bar")
):
    """ Function that does things and stuff """
    print(foo)
    print(bar)

if __name__ == '__main__':
    typer.run(main)

Теперь, вместо того, чтобы аргумент разрабатывал необходимость жить самостоятельно, где легко забыть и в конечном итоге получить неправильные аргументы, несоответствующие типы или помощь в текстовом виде, вместо этого аргументы CLI искажены из аргументов функции, включая подсказки типа, и вводится Тайпер! Это та инъекция, которую мы хотим повторить позже. Преимущество заключается в том, что аргументы CLI и документация живут вместе с аргументами функций, что делает его намного проще и аккуратно поддерживать.

Бег python typer_example.py -help Теперь генерирует следующее:

Usage: c.py [OPTIONS] FOO [BAR]

  Function that does things and stuff

Arguments:
  FOO    Required Foo  [required]
  [BAR]  Optional Bar  [default: 1]

Options:
  --install-completion  Install completion for the current shell.
  --show-completion     Show completion for the current shell, to copy it or
                        customize the installation.

  --help                Show this message and exit.

Полезно отметить, что вы можете запустить --Install-завершение Чтобы установить автозаполнение оболочки! Одна из многих вещей, которые Typer предоставляет в дополнение к основной обработке аргументов CLI.

Стоит отметить, что пожарная библиотека Google работает аналогичным образом.

Другим примером такого рода впрыска зависимостей на работе является FastAPI, высокоуровневой фреймворк HTTP-сервера, которая использует пакет под названием Pydantic , чтобы автоматически декодировать параметры, полезную нагрузку и пути запроса API, сохранив вас от большого количества котлера

Для сравнения, мы можем посмотреть на Flask, еще одну популярную структуру HTTP Server, которая часто является отправной точкой для новых студентов Python

Используя колбу

Простое приложение Flask для возврата простой полезной нагрузки может выглядеть так:

from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route("/foo/", methods=["post"])
def handle_foo(foo):
    payload = request.json
    bar = payload.get("bar", 1)
    return jsonify(foo=foo, bar=bar)

Здесь происходит несколько вещей, во -первых, foo взят с пути, поэтому, если бы я нажимал на сервер в /foo/привет тогда foo аргумент был бы Привет Анкет Это приятно, так как Флеска обрабатывает это для нас. Следующее, что ожидает полезной нагрузки JSON, и я постараюсь прочитать из него ключ «Бар», не выполняя значение значения 1. Так что в основном я хочу, чтобы полезная нагрузка выглядела как:

{
  "bar": 123
}

Но здесь не так много подтверждения. Если я предоставлю строку вместо Int, не будет ошибки. Если я предоставил дополнительные значения, ошибки нет. Хотя это тривиальный случай, чтобы проверить, представьте, что у вас есть десятки значений полезной нагрузки (потенциально даже вложенные структуры!), И вам не нужно было записывать это в большой цепочке Если петля.

Использование FASTAPI

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

from pydantic import BaseModel, Field
from fastapi import FastAPI

app = FastAPI()

class Payload(BaseModel):
    bar: int = Field(1, title="Optional Bar")

class RetModel(BaseModel):
    foo: str = Field(..., title="Result Foo")
    bar: int = Field(..., title="Result Bar")

@app.post("/foo/{foo}", response_model=RetModel)
async def handle_foo(foo: str, payload: Payload):
    return RetModel(foo=foo, bar=payload.bar)

Вот больше работы, предварительно определяющей кучу моделей, но это имеет несколько преимуществ:

  1. Pydantic будет использоваться для анализа данных. Если входящие данные являются неправильным форматом, будет сгенерировано сообщение об ошибке, которое точно объясняет, что с ним не так. Сохраняет вас от необходимости написать большую проверку данных кода
  2. Он автоматически вводится, поэтому вам не нужно обращаться с этим
  3. Документация может быть автоматически сгенерирована для вас!

Даже этот крошечный код генерирует полностью отложенную документацию по API, доступную при запуске сервера, в комплекте с описаниями и правильными типами переменных:

Таким образом, мы видели превосходную работу Tiangolo с инъекцией зависимости, используя возможности самоанализа Python для кода, чтобы рассмотреть аргументы вашей функции во время выполнения и выяснить, что хочет функция, и инъекция их.

Как вы можете видеть не только из примеров Fastapi и Typer, но и колга, что функция обернута декоратор что позволяет метапрограммировать, что -то, что на питоне довольно сильнее. Этот декоратор – это то, что позволяет нам сделать эту инъекцию, и где мы поместим код самоанализа.

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

Для этого мы сначала начнем с нашего предыдущего примера колбы:

from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route("/foo/", methods=["post"])
def handle_foo(foo):
    payload = request.json
    bar = payload.get("bar", 1)
    return jsonify(foo=foo, bar=bar)

Далее мы начнем создавать декоратор, который выполнит нашу работу в инъекцию, пустой декоратор, который ничего не делает, выглядит так:

from flask import Flask, request, jsonify
app = Flask(__name__)

def inject_pydantic_parse(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@app.route("/foo/", methods=["post"])
@inject_pydantic_parse
def handle_foo(foo):
    payload = request.json
    bar = payload.get("bar", 1)
    return jsonify(foo=foo, bar=bar)

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

from typing import get_type_hints
from flask import Flask, request, jsonify
from pydantic import BaseModel, Field
app = Flask(__name__)

class Payload(BaseModel):
    bar: int = Field(1, title="Optional Bar")

def inject_pydantic_parse(func):
    def wrapper(*args, **kwargs):
        for arg_name, arg_type in get_type_hints(func).items():
            parse_raw = getattr(arg_type, "parse_raw", None)
            if callable(parse_raw):
                kwargs[arg_name] = parse_raw(request.data)
        return func(*args, **kwargs)
    return wrapper

@app.route("/foo/", methods=["post"])
@inject_pydantic_parse
def handle_foo(foo, payload: Payload):
    return jsonify(foo=foo, bar=payload.bar)

Здесь этот цикл переходит по всем типам намека на функцию, и для каждого из них он проверит, чтобы увидеть, есть ли parse_raw Callable, и позвоните в это с request.data , вставая результаты в аргументы ключевого слова, которые будут использоваться для вызова функции позже. Это «инъекция»!

for arg_name, arg_type in get_type_hints(func).items():
    parse_raw = getattr(arg_type, "parse_raw", None)
    if callable(parse_raw):
        kwargs[arg_name] = parse_raw(request.data)

Наконец, поскольку эта обертка также может справиться с возвратной стоимостью, мы также можем справиться с этим аналогичным образом и собрать возвращаемое значение как модель Pydantic. Для этого нам нужен дополнительный слой упаковки из -за того, как Python обрабатывает аргументы декоратора. На самом деле есть более аккуратные способы сделать это с некоторыми встроенными библиотеками, но я покажу это здесь без:

from typing import get_type_hints
from flask import Flask, request
from pydantic import BaseModel, Field

app = Flask(__name__)

class Payload(BaseModel):
    bar: int = Field(1, title="Optional Bar")

class RetModel(BaseModel):
    foo: str = Field(..., title="Result Foo")
    bar: int = Field(..., title="Result Bar")

def inject_pydantic_parse(response_model):
    def wrap(func):
        def wrapped(*args, **kwargs):
            for arg_name, arg_type in get_type_hints(func).items():
                parse_raw = getattr(arg_type, "parse_raw", None)
                if callable(parse_raw):
                    kwargs[arg_name] = parse_raw(request.data)
            retval = func(*args, **kwargs)
            if isinstance(retval, response_model):
                return retval.dict()
            return retval

        return wrapped
    return wrap

@app.route("/foo/", methods=["post"])
@inject_pydantic_parse(response_model=RetModel)
def handle_foo(foo, payload: Payload):
    return RetModel(foo=foo, bar=payload.bar)

Теперь, аннотируя harder_foo () функция с @inject_pydantic_parse (response_model = retmodel) Обертка проверит, является ли объект возврата из handle_foo был действительно желаемым Respones_Model, а затем расшифровал это (в данном случае с использованием метода dict () , который имеют модели с пидантиками, который остальная часть колбы справится с превращением в JSON для нас). Выявление для выдвинутой модели ответа безопасно, поскольку она не мешает другим возможным возвращаемым значениям из функции.

Хотя это кажется много дополнительного кода, это inject_pydantic_parse () Функция может быть утиплена в утилит -модуле, и теперь вы можете украсить все конечные точки колбы в стиле Fastapi …

… хотя, не совсем. Это супер-просторный пример и не справляется с большим количеством краев. Чтобы использовать это по -настоящему, вам нужно добавить больше обработки ошибок и, возможно, объединить оба декоратора, чтобы вам понадобился только один. С таким же успехом вы могли бы просто переключиться на Fastapi, если у вас есть вариант! (Я не сделал, поскольку я использовал функции Google Cloud, и поэтому этот вариант очень помог мне)

Фотография обложки от Jr R на Неспособный

Оригинал: “https://dev.to/meseta/explaining-typer-and-fastapi-dependency-injection-and-rolling-your-own-in-python-2bf2”