FastAPI 2. Подготовка проекта
В этом посте мы рассмотрим принципы организации структуры проекта на FastAPI, упростим запуск приложения с помощью Poetry и создадим файл для переменных окружения, необходимых для подключения к PostgreSQL.
Реклама
Во многих уроках по 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
, к которому мы сможем обращаться из нашего кода.
Заключение
Продумывание архитектуры приложения – крайне важная задача, которой часто пренебрегают. В этот раз мы подходим к процессу основательно и делаем всё "по уму".
Все статьи