FastAPI 2. Подготовка проекта
Иван АшихминПост на сайте
Поддержать проект на Boosty
Поддержать проект в Telegram
Во многих уроках по FastAPI нас учат писать всё в одном файле main.py, лишь изредка создавая дополнительные файлы. Это помогает понять основы создания простейших маршрутов или подключения SQLAlchemy, но отрицательно сказывается на понимании архитектуры приложения, а также на читабельности и поддерживаемости кода.
В этом посте мы начнём разрабатывать архитектуру нашего будущего сервиса.
Прежде чем углубляться в детали архитектуры, упростим запуск приложения и создадим файл для переменных окружения.
Примечание: В проекте с самого начала будет использоваться СУБД PostgreSQL. Если у вас нет удалённого сервера с БД, то инструкцию, как запустить её на локальной машине с помощью Docker, я написал в посте: "Docker 3. Контейнер с PostgreSQL".
Запуск с использованием Poetry
Точка входа для Poetry
В прошлом посте мы запускали проект, вручную переходя в нужную директорию и, выполняя команду для запуска uvicorn. Давайте оптимизируем этот процесс.
Откроем файл main.py и под нашим тестовым маршрутом напишем функцию start. Она не принимает аргументов. Это будет наша "точка входа" в проект из Poetry.
В теле функции вызовем uvicorn с методом .run(). В него передадим два именованных аргумента:
app- в который передадим строку, содержащую имя файла, включая имя пакета и экземпляр классаFastAPI().reload- установим значениеTrue. Это позволит серверу автоматически перезапускать его, если в момент когда он запущен произойдут изменения файлов. По аналогии с тем, как это работает в Django.
Не забудьте импортировать uvicorn в начале файла!
Пример кода функции:
import uvicorn
def start():
uvicorn.run(app="lkeep.main:app", reload=True)Команда запуска
Теперь откроем файл pyproject.toml. После блока [tool.poetry.dependencies] добавим новый блок [tool.poetry.scripts].
В нём создадим переменную app, где укажем строку, аналогичную той, что использовалась выше в uvicorn, но с указанием функции start вместо app.
Пример кода нового блока:
[tool.poetry.scripts]
app = "lkeep.main:start"Что мы сделали? Мы указали, что при вызове команды poetry run app в терминале будет вызвана функция start() из файла main.py.
Проверка и чистка
Проверим, что всё работает корректно:
- Откройте терминал, убедитесь, что вы находитесь в корневой директории проекта.
- Выполните команду
poetry run app.
Если всё правильно, запустится сервер uvicorn, и появится ссылка для доступа к сайту. Откройте её. Если видите ответ от маршрута, значит, всё работает.
Остановите сервер сочетанием клавиш CTRL+C.
Откройте файл main.py и удалите тестовый маршрут, так как маршруты в главном файле обычно не рекомендуется хранить.
Пример финального содержимого main.py:
import uvicorn
from fastapi import FastAPI
app = FastAPI()
def start():
uvicorn.run(app="lkeep_fastapi.main:app", reload=True)Переменные среды для "секретов"
Нередко в коде необходимо прописывать "секретные" данные, такие как логины, пароли и токены. Эти данные принято "прятать" в переменные окружения. Если вы работаете над проектом в команде или планируете опубликовать его на GitHub, сокрытие важных данных становится обязательным.
Мы будем использовать файл .env для хранения переменных окружения.
- В корневой директории проекта создайте файл
.env. - Если у вас подключена система контроля версий (git), обязательно добавьте этот файл в
.gitignore.
Пример содержимого .env:
DB_NAME=db_name
DB_USER=db_user
DB_PASSWORD=db_password
DB_HOST=localhost
DB_PORT=5432
DB_ECHO=TrueМы прописали следующие параметры:
DB_NAME- имя базы данных.DB_USER- пользователь базы данных.DB_PASSWORD- пароль пользователя.DB_HOST- localhost если БД на локальной машине или адрес удалённого сервера.DB_PORT- порт базы данных, по умолчанию 5432.DB_ECHO- включение отладки для SQLAlchemy. При значенииTrueв консоль будут выводиться все запросы к БД. Это полезно при разработке, но очень сильно загружает систему. Удостоверьтесь, что эта функция отключена, когда в ней нет потребности.
С подготовкой закончили. Перейдём к созданию архитектуры.
Настройки подключения к БД
Для работы с базой данных будем использовать ORM SQLAlchemy, асинхронный клиент asyncpg и библиотеку pydantic-settings для управления настройками.
Описание библиотек
SQLAlchemy- это библиотека Python, используемая для работы с базами данных. Она позволяет представлять базы данных в виде объектов Python и облегчает выполнение таких операций, как вставка, обновление и удаление данных. SQLAlchemy также позволяет вам определять и выполнять запросы к базе данных на языке Python, вместо того чтобы писать SQL-код вручную. Это делает код более читаемым и упрощает его поддержку.asyncpg- это асинхронная библиотека Python для работы с базой данных PostgreSQL. Она предоставляет API, который позволяет выполнять операции с базой данных, такие, как выполнение запросов и обработка результатов в асинхронном режиме. Это означает, что код будет выполняться более эффективно, поскольку он не будет блокировать выполнение других задач, пока ожидает ответа от базы данных.Pydantic-Settings- это библиотека для работы с настройками в Python-проектах. Она позволяет определять структуру настроек, проверять их корректность, загружать настройки из различных источников и сохранять их в файл или базу данных.
Добавим их в проект выполнив команду:
poetry add sqlalchemy asyncpg pydantic-settingsСоздание настроек
В пакете lkeep, где находится файл main.py, создадим новый пакет core. Внутри создаём файл settings.py.
Класс DBSettings
Создаём класс DBSettings, унаследованный от BaseSettings из библиотеки pydantic-settings.
Внутри прописываем пять полей, совпадающих с параметрами в .env-файле: db_name, db_user, db_host, db_port, db_echo.
Обратите внимание, что библиотека pydantic-settings – это надстройка для библиотеки pydantic, входящей в состав FastAPI. В связи с этим в полях обязательно использование аннотации типов.
Большинство полей будут стандартных типов данных, таких как, int или str, однако два поля будут иметь специализированные Pydantic-типы:
Первым таким полем будет db_password с типом данных SecretStr. SecretStr это тип данных в Pydantic, предназначенный для хранения секретных данных. Значения этого типа скрыты в репрезентации и логах, что помогает повысить безопасность.
Вторым таким полем будет db_url с типом данных PostgresDsn | None и значением по умолчанию None. PostgresDsn - специальный тип данных в Pydantic для хранения и валидации URL-адресов PostgreSQL. Обеспечивает правильный синтаксис строки подключения к базе данных PostgreSQL.
С полем db_url есть интересный момент - его можно прописать двумя способами:
Собрать строку подключения к БД вручную в .env файле. Будет примерно так:
DB_URL=postgresql+asyncpg://db_user:db_password@db_host:db_port/db_nameОбратите внимание! В строке необходимо прописать данные для подключения, а не имена переменных!
Или собрать строку, используя метод .build из класса PostgresDsn.
Для этого после полей напишем конструктор класса __init__, принимающий self и возможные ключевые аргументы **kwargs.
Далее внутри метода первым делом инициализируем конструктор наследуемого класса, вызывая super() с методом __init__, передавая в него только возможные ключевые аргументы **kwargs.
Далее в блоке if проверяем, есть ли что-то в поле db_url, если нет, то попадаем внутрь условия.
Внутри условия прописываем поле класса self.db_url. В нём, у класса PostgresDsn, вызываем метод .build, передавая внутрь ряд аргументов:
scheme- Схема подключения к БД. В нашем случае, этоpostgresql+asyncpg.username- Аргумент, в который передаём поле с именем пользователя.password- Аргумент, в который передаём поле с паролем базы данных, но поскольку поле у нас типа данныхSecretStr, для того, чтобы получить значение, необходимо у поля вызвать метод.get_secret_value().host- Аргумент, в который передаём поле с хостом базы данных.port- Аргумент, в который передаём поле с портом базы данных.path- Аргумент, в который передаём поле с именем базы данных.
В таком случае, если мы пропишем db_url в .env-файле, то будет использоваться он, если же такого поля там не будет, то при создании экземпляра класса настроек, в конструкторе класса произойдёт сборка URL-адреса для базы данных.
Коротко про postgresql+asyncpg. Данная конструкция указывает, в нашем случае ORM SQLAlchemy, какая база данных используется и какой драйвер для подключения к ней. До символа разделителя +, указывается используемая база данных, например, postgresql, mysql и другие. После символа разделителя, указывается используемый для подключения к базе данных драйвер, проще говоря, название библиотеки, например, asyncpg, psycopg2 и другие.
Также, над конструктором класса, прописываем ещё одно поле - model_config, в котором определяем экземпляр класса SettingsConfigDict с тремя аргументами:
env_file- Путь до файла с переменными окружения.env_file_encoding- Указание на кодировку файла.extra- Указание, что делать с переменными не указанными в полях класса. В нашем случае - игнорировать.
Код:
from typing import Optional
from pydantic import SecretStr, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
class DBSettings(BaseSettings):
db_name: str
db_user: str
db_password: SecretStr
db_host: str
db_port: int
db_echo: bool
db_url: PostgresDsn | None = None
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf8", extra="ignore"
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.db_url:
self.db_url = PostgresDsn.build(
scheme="postgresql+asyncpg",
username=self.db_user,
password=self.db_password.get_secret_value(),
host=self.db_host,
port=self.db_port,
path=self.db_name,
)Класс Settings
Ниже создадим класс Settings, также унаследованный от BaseSettings.
В нём пока пропишем только одно поле db_settings, которое будет экземпляром класса DBSettings.
Вне классов, в самом низу создаём переменную settings и объявляем её экземпляром класса Settings.
class Settings(BaseSettings):
db_settings: DBSettings = DBSettings()
settings = Settings()При инициализации приложения в переменной settings будет создан объект класса Settings, к которому мы сможем обращаться из нашего кода.
Заключение
Продумывание архитектуры приложения – крайне важная задача, которой часто пренебрегают. В этот раз мы подходим к процессу основательно и делаем всё "по уму".