Сегодня я хочу попробовать что-то новое и начну исследовать мир Python. В этой статье представлен пошаговый туториал по реализации простого REST API при помощи Python, Fast API, Hydra и Mamba. Более того, я вкратце опишу, как упаковать всех этих змей в один образ Docker и заставить их работать вместе. Весь код выложен на моём GitHub.

Давайте начнём с кратного объяснения того, почему я решил выбрать эту тему.

▍ Почему это интересно?


Для начала, я хотел поделиться своими знаниями, поскольку недавно мне довелось реализовать REST API на основе Python. Выбор фреймворка оказался простым — FAST API. Также нам нужен был инструмент управления зависимостями, поэтому мы выбрали Mamba. Для управления конфигурациями и загрузкой мы решили выбрать Hydra. Нам показалось, что все эти инструменты хорошо работают и предоставляют все необходимые функции, поэтому такое сочетание казалось вполне приемлемым. К сожалению, когда мы перешли к интеграции инструментов и попытались заставить их совместно работать, всё оказалось не так просто. Более того, выяснилось, что информационных ресурсов и примеров по этой теме довольно мало. Именно поэтому я решил написать данную статью.

▍ Что такое Mamba?


Мамба — это смертельно опасный род змей. Ну, а если серьёзно, Mamba — это инструмент для управления зависимостями в проекте и для создания виртуальных окружений Python. Он создан на основе Anaconda, но должен был стать намного быстрее, и судя по моему краткому опыту работы с Mamba могу сказать, что он действительно быстр. Благодаря тому, что он разработан на основе Conda, у нас есть доступ ко всем готовым пакетам из репозиториев Conda. Более того, Mamba API в целом очень похож на Conda, что упрощает пользователям Conda переход на Mamba.

▍ Что такое Fast API?


Это инструмент, который, вероятно известен практически всем в сообществе Python: асинхронный, быстрый и нетребовательный к ресурсам инструмент для создания REST API. Наверно, сегодня это инструмент, который можно рекомендовать всем, кто хочет начать изучение Python и REST. Он содержит все функции для создания работающего API, вместе с WebSockets и поддержкой потоковой передачи. Более того, FAST API использует аннотации типов Python, поэтому автодополнение кода в IDE работает вполне неплохо (по крайней мере, в PyCharm). На мой взгляд, ещё одной довольно полезной функцией является встроенная поддержка swagger. На самом деле, меня удивило её наличие.

▍ Что такое Hydra?


Гидра — это чудовище со множеством голов из древнегреческой мифологии. Hydra — это опенсорсный инструмент для управления и выполнения конфигураций приложений на основе Python. Он основан на библиотеке Omega-Conf. Цитата с главной страницы инструмента: «Ключевой особенностью является возможность динамического создания иерархической конфигурации композированием и переопределением при помощи файлов конфигурации и командной строки». Для меня описанная в цитате иерархическая конфигурация оказалась очень полезной. В моём случае она работала вполне неплохо и обеспечивала более чёткое разделение файлов конфигурации.

▍ Реализация


1. Давайте приступим к проекту, создав environment.yaml с конфигурацией нашего окружения.

name: greeter-service
channels:
  - conda-forge
dependencies:
  - python=3.8.13
  - pip
  - fastapi
  - uvicorn[standard]
  - hydra-core
  - pytest

Файл содержит имя нашего нового виртуального окружения (greeter-service), а также источник, из которого нужно скачивать зависимости (conda-forge), и полный список зависимостей, требуемых для правильной работы приложения. Благодаря Mamba, я могу настроить всё окружение за считанные минуты, при помощи одной простой команды:

mamba env create -n greeter-service --file environment.yaml

При установке Mamba я рекомендую использовать туториал, написанный самими авторами Mamba.

2. На этом этапе я определю файл config.yaml со всей конфигурацией, необходимой приложению.

app:
  port: ${oc.env:GREETER_API_PORT,8070}
  version: ${oc.env:GREETER_API_VERSION,v1}
  greet_message: "Hello, "

Здесь нет ничего особенного, довольно простой файл .yaml с небольшой магией, подключенной при помощи считывания переменных окружения. Это вся конфигурация, которую я буду использовать в своём туториале. Структура довольно стандартна:

  • порт, на котором будет работать наш API
  • версия API, которая будет использоваться в конечных точках

Единственный нестандартный элемент — это параметр greet_message, содержащий основу сообщения, которое будет возвращаться пользователю.

3. Я добавляю файл config.py, отвечающий за считывание конфигурации Hydra.

import os

import hydra
from hydra import compose

hydra.initialize_config_dir(config_dir=os.getenv('GREETER_CONFIG_DIR'))

api_config = compose(config_name='config')

Сначала я инициализирую контекст Hydra на основании папки config по пути ./ . Hydra будет использовать переменные окружения или брать корневую папку проекта. Затем я использую метод композирования из Hydra для считывания конфигурации, определённой на предыдущем этапе.

4. Далее я реализую первую конечную точку API. Я задам её в файле health_check.py, поскольку она будет отвечать за обработку запросов проверки состояния.

from fastapi import APIRouter

health_router = APIRouter(prefix='/health', tags=['health_checks'])


@health_router.get('', status_code=200)
def is_ok():
    return 'Ok'

Код прост и понятен. Это просто роутер FAST API с одним методом, возвращающим при вызове Ok и код HTTP 200.

5. На этом этапе я создаю файл greeter.py, отвечающий за обработку входящих запросов.

from fastapi import APIRouter

from api.config import api_config

greeting_router = APIRouter(tags=['greet'])


@greeting_router.get('/greet/{name}', status_code=200)
def say_hello(name: str):
    return api_config.app.greet_message + name

Ещё одна простая базовая конечная точка FAST API, получающая на входе имя пользователя. Затем она соединяет переданное имя со считанным из конфигурации форматом сообщений и возвращает пользователю готовое сообщение с приветствием.

6. Теперь я реализую файл main.py, который связывается с роутерами из предыдущих этапов.

import uvicorn
from fastapi import FastAPI, APIRouter

from api.config import api_config
from api.greeter_api import greeting_router
from api.health_check import health_router

main_api = FastAPI()

main_router = APIRouter(prefix=f'/{api_config.app.version}')
main_router.include_router(health_router)
main_router.include_router(greeting_router)

main_api.include_router(main_router)


def start():
    uvicorn.run(main_api, host='0.0.0.0', port=api_config.app.port)

Это просто обычный код FAST API. Примечательно здесь то, что я добавляю версию как базовый префикс ко всем конечным точкам. Самый важный метод здесь — это метод start, в котором я вручную запускаю сервер uvicorn (это не опечатка, а настоящее имя сервера) на считанном из конфигурации порту.

Мой простой сервис готов к тестированию, но не бойтесь, это не конец нашего туториала. Теперь я расскажу о том, как сделать так, чтобы он работал в качестве образа Docker.

7. Эту часть я начну с определения файла setup.py.

from setuptools import setup

setup(
    name='greeter-service',
    version='1.0',
    packages=['api'],
    entry_points={
        'console_scripts': [
            'greeter-service = api.main:start',
        ]
    }
)

Самый важный параметр в этом скрипте — это entry_points; по сути, он определяет, какой метод Python отвечает за приложение. В данном случае это метод start из main.py. Также он определяет имя сервиса Python, который можно использовать для выполнения приложения из командной строки.

8. Настало время подготовить Dockerfile.

FROM condaforge/mambaforge

WORKDIR /greeter-service

COPY environment.yaml environment.yaml

RUN mamba env create -n greeter-service --file environment.yaml

COPY api api
COPY config config
COPY setup.py setup.py

ENV PATH /opt/conda/envs/greeter-service/bin:$PATH

RUN /bin/bash -c "source activate greeter-service" && python setup.py install

Что здесь происходит?

  • Для начала, я использую официальный образ Mamba, потому что не хочу тратить время на установку Mamba в Docker с нуля.
  • Затем я задаю greeter-service в качестве рабочей папки и добавляю CONFIG_DIR в качестве новой переменной окружения.
  • Эту переменную будет использовать Hydra как путь к файлам конфигурации приложения. На следующем этапе я скопировал файл окружения и использовал его для создания виртуального окружения Mamba в Docker.
  • Несколько следующих строк — это обычные копии папок с кодом приложения и конфигурацией.
  • Последние две строки — это своего рода хак для Conda, не особо хорошо работающего с Docker и в какой-то степени с самим шеллом.

Без этого хака мы бы видели такие сообщения об исключениях: Your shell has not been properly configured to use 'conda activate and you will not be able to execute conda activate greeter-service. К сожалению, похоже, ни одно другое исправление здесь не работает, по крайней мере, в моём случае. Некоторые ссылки по теме можно найти здесь, здесь и здесь.

9. Вишенкой на торте станет файл композирования Docker, сильно упрощающий настройку образа Docker.

version: "3.9"

services:
  greeter-api:
    build: .
    command: greeter-service
    ports:
      - "8070:8070"
    environment:
      - GREETER_CONFIG_DIR=/greeter-service/config
      - GREETER_API_PORT=8070

Пока это просто один сервис Docker с двумя использованными переменными окружения — путём к папке с конфигурацией и портом. Compose соберёт образ Docker на основании Dockerfile в локальной папке. Далее он использует greeter-service как команду запуска контейнера. Также он откроет и привяжет локальный порт 8070 с портом 8070 контейнера.

Вуаля, реализация готова. Настала пора выполнить тестирование.

▍ Тестирование


Как и в других своих статьях, для выполнения тестов работающего API я использую Postman. Давайте начнём тестирование с настройки образа Docker. Для настройки всего окружения тестирования достаточно простой команды docker compose build && docker compose up, по крайней мере, если всё работает, как задумано.


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


А теперь конечная точка health.


Немного логов из контейнера Docker, чтобы доказать, что мы не просто нарисовали эти скриншоты:


Мы завершили кодинг и тестирование, настала пора вкратце подвести итоги.

▍ Заключение


Интеграция была реализована, протестирована и описана при помощи не самых известных инструментов, которые я решил использовать. Пример достаточно прост, но содержит всё, что может понадобиться для запуска в любом месте. Более того, его можно легко расширить и использовать как фундамент для более сложных проектов. Надеюсь, статья была для вас полезной.
Конкурс статей от RUVDS.COM. Три денежные номинации. Главный приз — 100 000 рублей.

Комментарии (1)


  1. Antonto
    06.09.2022 12:54

    >Затем я задаю greeter-service в качестве рабочей папки и добавляю CONFID_DIR в качестве новой переменной окружения.

    ошибка в слове CONFIG, да и в Dockerfile этого нет - забыли добавить.