Cat

FastAPI 2. Подготовка проекта

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

 

 

Сервис на FastAPI proDream 05 Сентябрь 2024 Просмотров: 257

Во многих уроках по FastAPI нас учат писать всё в одном файле main.py, лишь изредка создавая дополнительные файлы. Это помогает понять основы создания простейших маршрутов или подключения SQLAlchemy, но отрицательно сказывается на понимании архитектуры приложения, а также на читабельности и поддерживаемости кода.

В этом посте мы начнём разрабатывать архитектуру нашего будущего сервиса.

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

Примечание: В проекте с самого начала будет использоваться СУБД PostgreSQL. Если у вас нет удалённого сервера с БД, то инструкцию, как запустить её на локальной машине с помощью Docker, я написал в посте: "Docker 3. Контейнер с PostgreSQL".

 

Запуск с использованием Poetry

 

Точка входа для Poetry

 

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

Откроем файл main.py и под нашим тестовым маршрутом напишем функцию start. Она не принимает аргументов. Это будет наша "точка входа" в проект из Poetry.

В теле функции вызовем uvicorn с методом .run(). В него передадим два именованных аргумента:

  1. app - в который передадим строку, содержащую имя файла, включая имя пакета и экземпляр класса FastAPI().
  2. 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.

 

Проверка и чистка

 

Проверим, что всё работает корректно:

  1. Откройте терминал, убедитесь, что вы находитесь в корневой директории проекта.
  2. Выполните команду 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 для хранения переменных окружения.

  1. В корневой директории проекта создайте файл .env.
  2. Если у вас подключена система контроля версий (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 для управления настройками.

 

Описание библиотек

 

  1. SQLAlchemy - это библиотека Python, используемая для работы с базами данных. Она позволяет представлять базы данных в виде объектов Python и облегчает выполнение таких операций, как вставка, обновление и удаление данных. SQLAlchemy также позволяет вам определять и выполнять запросы к базе данных на языке Python, вместо того чтобы писать SQL-код вручную. Это делает код более читаемым и упрощает его поддержку.
  2. asyncpg - это асинхронная библиотека Python для работы с базой данных PostgreSQL. Она предоставляет API, который позволяет выполнять операции с базой данных, такие, как выполнение запросов и обработка результатов в асинхронном режиме. Это означает, что код будет выполняться более эффективно, поскольку он не будет блокировать выполнение других задач, пока ожидает ответа от базы данных.
  3. 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, к которому мы сможем обращаться из нашего кода.

 

Заключение

 

Продумывание архитектуры приложения – крайне важная задача, которой часто пренебрегают. В этот раз мы подходим к процессу основательно и делаем всё "по уму".

Автор

    Нет комментариев

    Реклама