Книга: FastAPI: веб-разработка на Python
Назад: Глава 8. Веб-уровень
Дальше: Глава 10. Уровень данных

Глава 9. Сервисный уровень

Что это было в середине?

Отто Уэст (фильм «Рыбка по имени Ванда»)

Обзор

В этой главе рассказывается о сервисном уровне — среднем. Ущерб от протекающей крыши здания может вылиться в кругленькую сумму. Утечки в программном обеспечении не так очевидны, но исправление вызванных ими проблем может потребовать много времени и усилий. Как построить приложение так, чтобы на уровнях не возникало утечек? В частности, что должно и что не должно попадать на сервисный уровень, расположенный посередине?

Определение сервиса

Сервисный уровень — это сердце сайта, смысл его существования. Он принимает запросы из разных источников, получает доступ к данным, представляющим собой ДНК сайта, и возвращает ответы.

Общие шаблоны сервисов включают в себя сочетание следующих элементов:

• создать/извлечь/изменить (частично или полностью)/удалить;

• один/несколько элементов.

На уровне маршрутизатора RESTful существительные — это ресурсы. В этой книге ресурсы изначально будут включать в себя криптидов (воображаемых существ) и людей (исследователей криптидов). Позже можно будет определить связанные ресурсы, подобные следующим:

• локации;

• события (например, экспедиции, наблюдения).

Макет

Вот текущее расположение файлов и каталогов:

main.py web

├── __init__.py

├── creature.py ├── explorer.py service

├── __init__.py

├── creature.py ├── explorer.py

data

├── __init__.py

├── creature.py ├── explorer.py model

├── __init__.py

├── creature.py ├── explorer.py

fake

├── __init__.py

├── creature.py

├── explorer.py

└── test

В этой главе вы будете работать с файлами, находящимися в каталоге service.

Защита

Одна из приятных особенностей уровней заключается в том, что вам не нужно беспокоиться обо всем. Сервисный уровень заботится только о том, что входит на уровень данных и выходит с него. В главе 11 вы увидите, что более высокий уровень (здесь — веб-уровень) может справиться со всеми сложностями аутентификации и авторизации. Функции создания, изменения и удаления не должны быть широко открытыми, и даже для функций get со временем могут потребоваться некоторые ограничения.

Функции

Начнем с файла creature.py. На этом этапе потребности файла explorer.py будут почти такими же, и мы можем позаимствовать почти весь ранее написанный код. Так заманчиво написать один сервисный файл, работающий с обоими типами ресурсов, но почти неизбежно то, что в какой-то момент нам понадобится работать с ними по-разному.

Кроме того, сейчас сервисный файл представляет собой практически сквозной уровень. Это тот случай, когда создание небольшой дополнительной структуры на начальном этапе окупится впоследствии. Точно так же, как это делалось для файлов web/creature.py и web/explorer.py в главе 8, вы определите сервисные модули для обоих ресурсов и подключите их к соответствующим модулям фиктивных данных (примеры 9.1 и 9.2).

Пример 9.1. Начальный файл service/creature.py

from models.creature import Creature

import fake.creature as data

def get_all() -> list[Creature]:

    return data.get_all()

def get_one(name: str) -> Creature | None:

    return data.get(id)

def create(creature: Creature) -> Creature:

    return data.create(creature)

def replace(id, creature: Creature) -> Creature:

    return data.replace(id, creature)

def modify(id, creature: Creature) -> Creature:

    return data.modify(id, creature)

def delete(id, creature: Creature) -> bool:

    return data.delete(id)

Пример 9.2. Начальный файл service/explorer.py

from models.explorer import Explorer

import fake.explorer as data

def get_all() -> list[Explorer]:

    return data.get_all()

def get_one(name: str) -> Explorer | None:

    return data.get(name)

def create(explorer: Explorer) -> Explorer:

    return data.create(explorer)

def replace(id, explorer: Explorer) -> Explorer:

    return data.replace(id, explorer)

def modify(id, explorer: Explorer) -> Explorer:

    return data.modify(id, explorer)

def delete(id, explorer: Explorer) -> bool:

    return data.delete(id)

Синтаксис функции get_one(), возвращающий значение (Creature | None), требует наличия сборки Python начиная с версии 3.9. Для более ранних версий вам потребуется код Optional:

from typing import Optional

...

def get_one(name: str) -> Optional[Creature]:

...

Тестируем!

Теперь, когда кодовая база немного наполнилась, самое время внедрить автоматизированные тесты. (Все веб-тесты в предыдущей главе выполнялись вручную.) Итак, создадим несколько каталогов:

test — каталог верхнего уровня наряду с web, service, data и model;

• unit — проверка отдельных функций без пересечения границ уровня;

• web — модульные тесты веб-уровня;

• service — модульные тесты сервисного уровня;

• data — модульные тесты уровня данных;

full — также известны как сквозные или контрактные тесты, они охватывают все уровни сразу и обращаются к конечным точкам API на веб-уровне.

У каталогов будет префикс test_ или суффикс _test для использования pytest, что показано в примере 9.4 (в нем выполняется тест из примера 9.3).

Прежде чем приступить к тестированию, необходимо выбрать несколько вариантов дизайна API. Что должна возвращать функция get_one() при отсутствии совпадений для ресурсов Creature или Explorer? Можно вернуть значение None, как в примере 9.2. Или вызвать исключение. Ни один из встроенных типов исключений Python не работает напрямую с отсутствующими значениями.

TypeError может оказаться наиболее близким по смыслу, поскольку типы None и Creature различаются.

• ValueError больше подходит для неправильного значения для данного типа, но, наверное, можно сказать, что передача отсутствующей строки id в функцию get_one(id) подходит.

• Вы можете определить собственный тип MissingError, если действительно хотите этого.

Какой бы способ вы ни выбрали, результат будет сказываться на верхнем уровне.

Пока остановимся на варианте с None, а не на исключении. В конце концов, слово none означает именно отсутствие чего-то. Пример 9.3 представляет собой тест.

Пример 9.3. Сервисный тест test/unit/service/test_creature.py

from model.creature import Creature

from service import creature as code

sample = Creature(name="yeti",

        country="CN",

        area="Himalayas",

        description="Hirsute Himalayan",

        aka="Abominable Snowman",

        )

def test_create():

    resp = code.create(sample)

    assert resp == sample

def test_get_exists():

    resp = code.get_one("yeti")

    assert resp == sample

def test_get_missing():

    resp = code.get_one("boxturtle")

    assert data is None

Запустите тест из примера 9.4.

Пример 9.4. Запуск сервисного теста

$ pytest -v test/unit/service/test_creature.py

test_creature.py::test_create PASSED                         [ 16%]

test_creature.py::test_get_exists PASSED                     [ 50%]

test_creature.py::test_get_missing PASSED                    [ 66%]

======================== 3 passed in 0.06s =========================

В главе 10 функция get_one() больше не будет возвращать значение None для отсутствующего существа, а выполнение теста test_get_missing() из примера 9.4 станет выдавать сбой. Но это будет исправлено.

Другие нюансы сервисного уровня

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

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

• ведение журналов;

• получение метрик;

• мониторинг;

• трассировка.

В этом разделе рассмотрим каждую из них. И вернемся к этим параметрам в разделе «Устранение неполадок» (в главе 13), чтобы узнать, могут ли они помочь в диагностике проблем.

Ведение журналов

FastAPI регистрирует каждый вызов API к конечной точке, включая метку времени, метод и URL-адрес, но не любые данные, переданные в теле или заголовках.

Метрики, мониторинг, наблюдаемость

Если у вас есть сайт, вы наверняка хотите знать, как он работает. Для веб-сайта с API может потребоваться узнать, к каким конечным точкам обращаются, сколько человек их посещают и т.д. Статистические данные о таких факторах называются метриками, а их сбор — мониторингом или наблюдением.

В настоящее время популярными инструментами для работы с метриками являются Prometheus (https://prometheus.io), предназначенный для сбора метрик, и Grafana (https://grafana.com) для их отображения.

Трассировка

Хорошо ли работает ваш сайт? Часто бывает, что метрики в целом хороши, но результаты то тут, то там разочаровывают. Или весь сайт может работать неудовлетворительно. В любом случае полезно иметь инструмент, измеряющий количество времени, затраченное на вызов API от начала и до конца, и не только общую продолжительность, но и длительность каждого промежуточного этапа. Если что-то работает медленно, вы можете найти слабое звено в цепи. Это называется трассировкой.

Новый проект с открытым исходным кодом взял на вооружение более ранние продукты для трассировки, такие как Jaeger (https://www.jaegertracing.io), и назвал их OpenTelemetry (https://opentelemetry.io). Он включает в себя Python API (https://oreil.ly/gyL70) и по крайней мере одну интеграцию с FastAPI (https://oreil.ly/L6RXV).

Чтобы установить и настроить OpenTelemetry с помощью Python, следуйте инструкциям, приводимым в документации OpenTelemetry Python (https://oreil.ly/MBgd5).

Другие возможности

Эксплуатационные вопросы будут рассмотрены в главе 13. Как насчет наших доменов-криптидов и всего, что с ними связано? Помимо голых подробностей об исследователях и существах, что еще вы могли бы взять на вооружение? У вас могут появиться новые идеи, требующие внесения изменений в модели и другие уровни. Можете попробовать вот такие:

• связь исследователей с существами, которых они ищут;

• данные наблюдений;

• экспедиции;

• фото и видео;

• кружки и футболки с изображением снежного человека.

Каждая из этих категорий, как правило, требует определения одной или нескольких новых моделей, а также новых модулей и функций. Некоторые из них будут добавлены в часть IV книги, представляющую собой галерею приложений, добавленных к базе, созданной в части III.

Заключение

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

Назад: Глава 8. Веб-уровень
Дальше: Глава 10. Уровень данных